Introduce PushHistoryService to store data about the received push (#4573)
* Introduce PushHistoryService to store data about the received push Add a push database. * Update screenshots * Improve preview. * Update screenshots * Add missing test. * Add test for PushHistoryView * Fix configuration issue. Was: w: /libraries/troubleshoot/impl/src/test/kotlin/io/element/android/libraries/troubleshoot/impl/history/PushHistoryPresenterTest.kt:35:27 Cannot access class 'PushProvider' in the expression type. While it may work, this case indicates a configuration mistake and can lead to avoidable compilation errors, so it may be forbidden soon. Check your module classpath for missing or conflicting dependencies. --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
parent
a0b619007a
commit
a1d8322738
60 changed files with 1656 additions and 214 deletions
|
|
@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.GetCurrentPushProvider
|
||||
import io.element.android.libraries.push.api.PushService
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.push.impl.store.PushDataStore
|
||||
import io.element.android.libraries.push.impl.test.TestPush
|
||||
import io.element.android.libraries.pushproviders.api.Distributor
|
||||
import io.element.android.libraries.pushproviders.api.PushProvider
|
||||
|
|
@ -34,6 +36,7 @@ class DefaultPushService @Inject constructor(
|
|||
private val getCurrentPushProvider: GetCurrentPushProvider,
|
||||
private val sessionObserver: SessionObserver,
|
||||
private val pushClientSecretStore: PushClientSecretStore,
|
||||
private val pushDataStore: PushDataStore,
|
||||
) : PushService, SessionListener {
|
||||
init {
|
||||
observeSessions()
|
||||
|
|
@ -125,4 +128,14 @@ class DefaultPushService @Inject constructor(
|
|||
pushClientSecretStore.resetSecret(sessionId)
|
||||
userPushStore.reset()
|
||||
}
|
||||
|
||||
override val pushCounter: Flow<Int> = pushDataStore.pushCounterFlow
|
||||
|
||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||
return pushDataStore.getPushHistoryItemsFlow()
|
||||
}
|
||||
|
||||
override suspend fun resetPushHistory() {
|
||||
pushDataStore.reset()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.history
|
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.impl.PushDatabase
|
||||
import io.element.android.libraries.push.impl.db.PushHistory
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushHistoryService @Inject constructor(
|
||||
private val pushDatabase: PushDatabase,
|
||||
private val systemClock: SystemClock,
|
||||
) : PushHistoryService {
|
||||
override fun onPushReceived(
|
||||
providerInfo: String,
|
||||
eventId: EventId?,
|
||||
roomId: RoomId?,
|
||||
sessionId: SessionId?,
|
||||
hasBeenResolved: Boolean,
|
||||
comment: String?,
|
||||
) {
|
||||
pushDatabase.pushHistoryQueries.insertPushHistory(
|
||||
PushHistory(
|
||||
pushDate = systemClock.epochMillis(),
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId?.value,
|
||||
roomId = roomId?.value,
|
||||
sessionId = sessionId?.value,
|
||||
hasBeenResolved = if (hasBeenResolved) 1 else 0,
|
||||
comment = comment,
|
||||
)
|
||||
)
|
||||
|
||||
// Keep only the last 100 events
|
||||
pushDatabase.pushHistoryQueries.removeOldest(100)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.history
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
|
||||
interface PushHistoryService {
|
||||
/**
|
||||
* Create a new push history entry.
|
||||
* Do not use directly, prefer using the extension functions.
|
||||
*/
|
||||
fun onPushReceived(
|
||||
providerInfo: String,
|
||||
eventId: EventId?,
|
||||
roomId: RoomId?,
|
||||
sessionId: SessionId?,
|
||||
hasBeenResolved: Boolean,
|
||||
comment: String?,
|
||||
)
|
||||
}
|
||||
|
||||
fun PushHistoryService.onInvalidPushReceived(
|
||||
providerInfo: String,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = null,
|
||||
roomId = null,
|
||||
sessionId = null,
|
||||
hasBeenResolved = false,
|
||||
comment = "Invalid push data",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onUnableToRetrieveSession(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
reason: String,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId,
|
||||
roomId = roomId,
|
||||
sessionId = null,
|
||||
hasBeenResolved = false,
|
||||
comment = "Unable to retrieve session: $reason",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
sessionId: SessionId,
|
||||
reason: String,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId,
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
hasBeenResolved = false,
|
||||
comment = "Unable to resolve event: $reason",
|
||||
)
|
||||
|
||||
fun PushHistoryService.onSuccess(
|
||||
providerInfo: String,
|
||||
eventId: EventId,
|
||||
roomId: RoomId,
|
||||
sessionId: SessionId,
|
||||
comment: String?,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = eventId,
|
||||
roomId = roomId,
|
||||
sessionId = sessionId,
|
||||
hasBeenResolved = true,
|
||||
comment = buildString {
|
||||
append("Success")
|
||||
if (comment.isNullOrBlank().not()) {
|
||||
append(" - $comment")
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
fun PushHistoryService.onDiagnosticPush(
|
||||
providerInfo: String,
|
||||
) = onPushReceived(
|
||||
providerInfo = providerInfo,
|
||||
eventId = null,
|
||||
roomId = null,
|
||||
sessionId = null,
|
||||
hasBeenResolved = true,
|
||||
comment = "Diagnostic push",
|
||||
)
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.history.di
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.anvil.annotations.ContributesTo
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.push.impl.PushDatabase
|
||||
import io.element.encrypteddb.SqlCipherDriverFactory
|
||||
import io.element.encrypteddb.passphrase.RandomSecretPassphraseProvider
|
||||
|
||||
@Module
|
||||
@ContributesTo(AppScope::class)
|
||||
object PushHistoryModule {
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providePushDatabase(
|
||||
@ApplicationContext context: Context,
|
||||
): PushDatabase {
|
||||
val name = "push_database"
|
||||
val secretFile = context.getDatabasePath("$name.key")
|
||||
|
||||
// Make sure the parent directory of the key file exists, otherwise it will crash in older Android versions
|
||||
val parentDir = secretFile.parentFile
|
||||
if (parentDir != null && !parentDir.exists()) {
|
||||
parentDir.mkdirs()
|
||||
}
|
||||
|
||||
val passphraseProvider = RandomSecretPassphraseProvider(context, secretFile)
|
||||
val driver = SqlCipherDriverFactory(passphraseProvider)
|
||||
.create(PushDatabase.Schema, "$name.db", context)
|
||||
return PushDatabase(driver)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,16 +30,26 @@ interface CallNotificationEventResolver {
|
|||
* @param forceNotify `true` to force the notification to be non-ringing, `false` to use the default behaviour. Default is `false`.
|
||||
* @return a [NotifiableEvent] if the notification data is a call notification, null otherwise
|
||||
*/
|
||||
fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean = false): NotifiableEvent?
|
||||
fun resolveEvent(
|
||||
sessionId: SessionId,
|
||||
notificationData: NotificationData,
|
||||
forceNotify: Boolean = false,
|
||||
): Result<NotifiableEvent>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultCallNotificationEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
) : CallNotificationEventResolver {
|
||||
override fun resolveEvent(sessionId: SessionId, notificationData: NotificationData, forceNotify: Boolean): NotifiableEvent? {
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify ?: return null
|
||||
return notificationData.run {
|
||||
override fun resolveEvent(
|
||||
sessionId: SessionId,
|
||||
notificationData: NotificationData,
|
||||
forceNotify: Boolean
|
||||
): Result<NotifiableEvent> = runCatching {
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify
|
||||
?: throw ResolvingException("content is not a call notify")
|
||||
|
||||
notificationData.run {
|
||||
if (NotifiableRingingCallEvent.shouldRing(content.type, timestamp) && !forceNotify) {
|
||||
NotifiableRingingCallEvent(
|
||||
sessionId = sessionId,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
|
|
@ -59,7 +60,7 @@ private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.No
|
|||
* this pattern allow decoupling between the object responsible of displaying notifications and the matrix sdk.
|
||||
*/
|
||||
interface NotifiableEventResolver {
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent?
|
||||
suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent>
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
|
|
@ -73,31 +74,39 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): ResolvedPushEvent? {
|
||||
override suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<ResolvedPushEvent> {
|
||||
// Restore session
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return Result.failure(
|
||||
ResolvingException("Unable to restore session for $sessionId")
|
||||
)
|
||||
val notificationService = client.notificationService()
|
||||
val notificationData = notificationService.getNotification(
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
).onFailure {
|
||||
Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.")
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
|
||||
return notificationData?.asNotifiableEvent(client, sessionId)
|
||||
return notificationData.flatMap {
|
||||
if (it == null) {
|
||||
Timber.tag(loggerTag.value).d("No notification data found for event $eventId")
|
||||
return@flatMap Result.failure(ResolvingException("Unable to resolve event"))
|
||||
} else {
|
||||
it.asNotifiableEvent(client, sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun NotificationData.asNotifiableEvent(
|
||||
client: MatrixClient,
|
||||
userId: SessionId,
|
||||
): ResolvedPushEvent? {
|
||||
val content = this.content
|
||||
val notifiableEvent = when (content) {
|
||||
): Result<ResolvedPushEvent> = runCatching {
|
||||
when (val content = this.content) {
|
||||
is NotificationContent.MessageLike.RoomMessage -> {
|
||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||
val messageBody = descriptionFromMessageContent(content, senderDisambiguatedDisplayName)
|
||||
buildNotifiableMessageEvent(
|
||||
val notifiableMessageEvent = buildNotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
|
|
@ -115,10 +124,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
senderAvatarPath = senderAvatarUrl,
|
||||
hasMentionOrReply = hasMention,
|
||||
)
|
||||
ResolvedPushEvent.Event(notifiableMessageEvent)
|
||||
}
|
||||
is NotificationContent.Invite -> {
|
||||
val senderDisambiguatedDisplayName = getDisambiguatedDisplayName(content.senderId)
|
||||
InviteNotifiableEvent(
|
||||
val inviteNotifiableEvent = InviteNotifiableEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
|
|
@ -136,15 +146,16 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
// TODO check if title is needed anymore
|
||||
title = null,
|
||||
)
|
||||
ResolvedPushEvent.Event(inviteNotifiableEvent)
|
||||
}
|
||||
NotificationContent.MessageLike.CallAnswer,
|
||||
NotificationContent.MessageLike.CallCandidates,
|
||||
NotificationContent.MessageLike.CallHangup -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||
null
|
||||
throw ResolvingException("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||
}
|
||||
is NotificationContent.MessageLike.CallInvite -> {
|
||||
buildNotifiableMessageEvent(
|
||||
val notifiableMessageEvent = buildNotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
|
|
@ -158,9 +169,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
)
|
||||
ResolvedPushEvent.Event(notifiableMessageEvent)
|
||||
}
|
||||
is NotificationContent.MessageLike.CallNotify -> {
|
||||
callNotificationEventResolver.resolveEvent(userId, this)
|
||||
val notifiableEvent = callNotificationEventResolver.resolveEvent(userId, this).getOrThrow()
|
||||
ResolvedPushEvent.Event(notifiableEvent)
|
||||
}
|
||||
NotificationContent.MessageLike.KeyVerificationAccept,
|
||||
NotificationContent.MessageLike.KeyVerificationCancel,
|
||||
|
|
@ -168,11 +181,12 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
NotificationContent.MessageLike.KeyVerificationKey,
|
||||
NotificationContent.MessageLike.KeyVerificationMac,
|
||||
NotificationContent.MessageLike.KeyVerificationReady,
|
||||
NotificationContent.MessageLike.KeyVerificationStart -> null.also {
|
||||
NotificationContent.MessageLike.KeyVerificationStart -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for verification ${content.javaClass.simpleName}")
|
||||
throw ResolvingException("Ignoring notification for verification ${content.javaClass.simpleName}")
|
||||
}
|
||||
is NotificationContent.MessageLike.Poll -> {
|
||||
buildNotifiableMessageEvent(
|
||||
val notifiableEventMessage = buildNotifiableMessageEvent(
|
||||
sessionId = userId,
|
||||
senderId = content.senderId,
|
||||
roomId = roomId,
|
||||
|
|
@ -187,19 +201,35 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
roomAvatarPath = roomAvatarUrl,
|
||||
senderAvatarPath = senderAvatarUrl,
|
||||
)
|
||||
ResolvedPushEvent.Event(notifiableEventMessage)
|
||||
}
|
||||
is NotificationContent.MessageLike.ReactionContent -> null.also {
|
||||
is NotificationContent.MessageLike.ReactionContent -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for reaction")
|
||||
throw ResolvingException("Ignoring notification for reaction")
|
||||
}
|
||||
NotificationContent.MessageLike.RoomEncrypted -> fallbackNotifiableEvent(userId, roomId, eventId).also {
|
||||
NotificationContent.MessageLike.RoomEncrypted -> {
|
||||
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
|
||||
val fallbackNotifiableEvent = fallbackNotifiableEvent(userId, roomId, eventId)
|
||||
ResolvedPushEvent.Event(fallbackNotifiableEvent)
|
||||
}
|
||||
is NotificationContent.MessageLike.RoomRedaction -> {
|
||||
// Note: this case will be handled below
|
||||
null
|
||||
val redactedEventId = content.redactedEventId
|
||||
if (redactedEventId == null) {
|
||||
Timber.tag(loggerTag.value).d("redactedEventId is null.")
|
||||
throw ResolvingException("redactedEventId is null")
|
||||
} else {
|
||||
ResolvedPushEvent.Redaction(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
redactedEventId = redactedEventId,
|
||||
reason = content.reason,
|
||||
)
|
||||
}
|
||||
}
|
||||
NotificationContent.MessageLike.Sticker -> null.also {
|
||||
NotificationContent.MessageLike.Sticker -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
|
||||
throw ResolvingException("Ignoring notification for reaction")
|
||||
}
|
||||
is NotificationContent.StateEvent.RoomMemberContent,
|
||||
NotificationContent.StateEvent.PolicyRuleRoom,
|
||||
|
|
@ -221,29 +251,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
NotificationContent.StateEvent.RoomTombstone,
|
||||
NotificationContent.StateEvent.RoomTopic,
|
||||
NotificationContent.StateEvent.SpaceChild,
|
||||
NotificationContent.StateEvent.SpaceParent -> null.also {
|
||||
NotificationContent.StateEvent.SpaceParent -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for state event ${content.javaClass.simpleName}")
|
||||
throw ResolvingException("Ignoring notification for state event ${content.javaClass.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
return if (notifiableEvent != null) {
|
||||
ResolvedPushEvent.Event(notifiableEvent)
|
||||
} else if (content is NotificationContent.MessageLike.RoomRedaction) {
|
||||
val redactedEventId = content.redactedEventId
|
||||
if (redactedEventId == null) {
|
||||
Timber.tag(loggerTag.value).d("redactedEventId is null.")
|
||||
null
|
||||
} else {
|
||||
ResolvedPushEvent.Redaction(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
redactedEventId = redactedEventId,
|
||||
reason = content.reason,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun fallbackNotifiableEvent(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
|||
notificationData = notificationData,
|
||||
// Make sure the notifiable event is not a ringing one
|
||||
forceNotify = true,
|
||||
)
|
||||
).getOrNull()
|
||||
notifiableEvent?.let { defaultNotificationDrawerManager.onNotifiableEventReceived(it) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.notifications
|
||||
|
||||
class ResolvingException(message: String) : Exception(message)
|
||||
|
|
@ -14,6 +14,12 @@ import io.element.android.libraries.core.log.logger.LoggerTag
|
|||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.push.impl.history.PushHistoryService
|
||||
import io.element.android.libraries.push.impl.history.onDiagnosticPush
|
||||
import io.element.android.libraries.push.impl.history.onInvalidPushReceived
|
||||
import io.element.android.libraries.push.impl.history.onSuccess
|
||||
import io.element.android.libraries.push.impl.history.onUnableToResolveEvent
|
||||
import io.element.android.libraries.push.impl.history.onUnableToRetrieveSession
|
||||
import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
|
|
@ -43,13 +49,15 @@ class DefaultPushHandler @Inject constructor(
|
|||
private val diagnosticPushHandler: DiagnosticPushHandler,
|
||||
private val elementCallEntryPoint: ElementCallEntryPoint,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
private val pushHistoryService: PushHistoryService,
|
||||
) : PushHandler {
|
||||
/**
|
||||
* Called when message is received.
|
||||
*
|
||||
* @param pushData the data received in the push.
|
||||
* @param providerInfo the provider info.
|
||||
*/
|
||||
override suspend fun handle(pushData: PushData) {
|
||||
override suspend fun handle(pushData: PushData, providerInfo: String) {
|
||||
Timber.tag(loggerTag.value).d("## handling pushData: ${pushData.roomId}/${pushData.eventId}")
|
||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||
Timber.tag(loggerTag.value).d("## pushData: $pushData")
|
||||
|
|
@ -57,18 +65,25 @@ class DefaultPushHandler @Inject constructor(
|
|||
incrementPushDataStore.incrementPushCounter()
|
||||
// Diagnostic Push
|
||||
if (pushData.eventId == DefaultTestPush.TEST_EVENT_ID) {
|
||||
pushHistoryService.onDiagnosticPush(providerInfo)
|
||||
diagnosticPushHandler.handlePush()
|
||||
} else {
|
||||
handleInternal(pushData)
|
||||
handleInternal(pushData, providerInfo)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun handleInvalid(providerInfo: String) {
|
||||
incrementPushDataStore.incrementPushCounter()
|
||||
pushHistoryService.onInvalidPushReceived(providerInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal receive method.
|
||||
*
|
||||
* @param pushData Object containing message data.
|
||||
* @param providerInfo the provider info.
|
||||
*/
|
||||
private suspend fun handleInternal(pushData: PushData) {
|
||||
private suspend fun handleInternal(pushData: PushData, providerInfo: String) {
|
||||
try {
|
||||
if (buildMeta.lowPrivacyLoggingEnabled) {
|
||||
Timber.tag(loggerTag.value).d("## handleInternal() : $pushData")
|
||||
|
|
@ -77,42 +92,77 @@ class DefaultPushHandler @Inject constructor(
|
|||
}
|
||||
val clientSecret = pushData.clientSecret
|
||||
// clientSecret should not be null. If this happens, restore default session
|
||||
val userId = clientSecret
|
||||
?.let {
|
||||
// Get userId from client secret
|
||||
pushClientSecret.getUserIdFromSecret(clientSecret)
|
||||
var reason = if (clientSecret == null) "No client secret" else ""
|
||||
val userId = clientSecret?.let {
|
||||
// Get userId from client secret
|
||||
pushClientSecret.getUserIdFromSecret(clientSecret).also {
|
||||
if (it == null) {
|
||||
reason = "Unable to get userId from client secret"
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
matrixAuthenticationService.getLatestSessionId()
|
||||
}
|
||||
if (userId == null) {
|
||||
Timber.w("Unable to get a session")
|
||||
return
|
||||
}
|
||||
val resolvedPushEvent = notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId)
|
||||
when (resolvedPushEvent) {
|
||||
null -> Timber.tag(loggerTag.value).w("Unable to get a notification data")
|
||||
is ResolvedPushEvent.Event -> {
|
||||
when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
|
||||
is NotifiableRingingCallEvent -> {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
handleRingingCallEvent(notifiableEvent)
|
||||
}
|
||||
else -> {
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
}
|
||||
?: run {
|
||||
matrixAuthenticationService.getLatestSessionId().also {
|
||||
if (it == null) {
|
||||
if (reason.isNotEmpty()) reason += " - "
|
||||
reason += "Unable to get latest sessionId"
|
||||
}
|
||||
}
|
||||
}
|
||||
is ResolvedPushEvent.Redaction -> {
|
||||
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
|
||||
}
|
||||
if (userId == null) {
|
||||
Timber.w("Unable to get a session")
|
||||
pushHistoryService.onUnableToRetrieveSession(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
reason = reason,
|
||||
)
|
||||
return
|
||||
}
|
||||
notifiableEventResolver.resolveEvent(userId, pushData.roomId, pushData.eventId).fold(
|
||||
onSuccess = { resolvedPushEvent ->
|
||||
pushHistoryService.onSuccess(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
sessionId = userId,
|
||||
comment = resolvedPushEvent.javaClass.simpleName,
|
||||
)
|
||||
|
||||
when (resolvedPushEvent) {
|
||||
is ResolvedPushEvent.Event -> {
|
||||
when (val notifiableEvent = resolvedPushEvent.notifiableEvent) {
|
||||
is NotifiableRingingCallEvent -> {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
handleRingingCallEvent(notifiableEvent)
|
||||
}
|
||||
else -> {
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(userId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
if (areNotificationsEnabled) {
|
||||
onNotifiableEventReceived.onNotifiableEventReceived(notifiableEvent)
|
||||
} else {
|
||||
Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is ResolvedPushEvent.Redaction -> {
|
||||
onRedactedEventReceived.onRedactedEventReceived(resolvedPushEvent)
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure = { failure ->
|
||||
Timber.tag(loggerTag.value).w(failure, "Unable to get a notification data")
|
||||
pushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo = providerInfo,
|
||||
eventId = pushData.eventId,
|
||||
roomId = pushData.roomId,
|
||||
sessionId = userId,
|
||||
reason = failure.message ?: failure.javaClass.simpleName,
|
||||
)
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.tag(loggerTag.value).e(e, "## handleInternal() failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,20 @@ import androidx.datastore.preferences.core.Preferences
|
|||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import app.cash.sqldelight.coroutines.asFlow
|
||||
import app.cash.sqldelight.coroutines.mapToList
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.push.api.store.PushDataStore
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import io.element.android.libraries.push.impl.PushDatabase
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
|
@ -28,6 +37,9 @@ private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(na
|
|||
@ContributesBinding(AppScope::class)
|
||||
class DefaultPushDataStore @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val pushDatabase: PushDatabase,
|
||||
private val dateFormatter: DateFormatter,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : PushDataStore {
|
||||
private val pushCounter = intPreferencesKey("push_counter")
|
||||
|
||||
|
|
@ -41,4 +53,35 @@ class DefaultPushDataStore @Inject constructor(
|
|||
settings[pushCounter] = currentCounterValue + 1
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>> {
|
||||
return pushDatabase.pushHistoryQueries.selectAll()
|
||||
.asFlow()
|
||||
.mapToList(dispatchers.io)
|
||||
.map { items ->
|
||||
items.map { pushHistory ->
|
||||
PushHistoryItem(
|
||||
pushDate = pushHistory.pushDate,
|
||||
formattedDate = dateFormatter.format(
|
||||
timestamp = pushHistory.pushDate,
|
||||
mode = DateFormatterMode.Full,
|
||||
useRelative = false,
|
||||
),
|
||||
providerInfo = pushHistory.providerInfo,
|
||||
eventId = pushHistory.eventId?.let { EventId(it) },
|
||||
roomId = pushHistory.roomId?.let { RoomId(it) },
|
||||
sessionId = pushHistory.sessionId?.let { SessionId(it) },
|
||||
hasBeenResolved = pushHistory.hasBeenResolved == 1L,
|
||||
comment = pushHistory.comment,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun reset() {
|
||||
pushDatabase.pushHistoryQueries.removeAll()
|
||||
context.dataStore.edit {
|
||||
it.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.push.impl.store
|
||||
|
||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface PushDataStore {
|
||||
val pushCounterFlow: Flow<Int>
|
||||
|
||||
/**
|
||||
* Get a flow of list of [PushHistoryItem].
|
||||
*/
|
||||
fun getPushHistoryItemsFlow(): Flow<List<PushHistoryItem>>
|
||||
|
||||
/**
|
||||
* Reset the push counter to 0, and clear the database.
|
||||
*/
|
||||
suspend fun reset()
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
CREATE TABLE PushHistory (
|
||||
pushDate INTEGER NOT NULL,
|
||||
providerInfo TEXT NOT NULL,
|
||||
eventId TEXT,
|
||||
roomId TEXT,
|
||||
sessionId TEXT,
|
||||
hasBeenResolved INTEGER NOT NULL,
|
||||
comment TEXT
|
||||
);
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM PushHistory ORDER BY pushDate DESC;
|
||||
|
||||
insertPushHistory:
|
||||
INSERT INTO PushHistory VALUES ?;
|
||||
|
||||
removeAll:
|
||||
DELETE FROM PushHistory;
|
||||
|
||||
-- add query to keep only the last x entries
|
||||
removeOldest:
|
||||
DELETE FROM PushHistory WHERE rowid NOT IN (SELECT rowid FROM PushHistory ORDER BY pushDate DESC LIMIT ?);
|
||||
Loading…
Add table
Add a link
Reference in a new issue