Update dependency org.matrix.rustcomponents:sdk-android to v25.7.7 (#4989)
Make sure we distinguish between notification events that were filtered out and those that couldn't be resolved. --- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
parent
f4032e291b
commit
4b10920507
18 changed files with 313 additions and 200 deletions
|
|
@ -12,6 +12,7 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
|
|||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClientProvider
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
||||
import io.element.android.libraries.matrix.api.notification.CallNotifyType
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationData
|
||||
|
|
@ -57,7 +58,7 @@ class DefaultCallNotificationEventResolver @Inject constructor(
|
|||
forceNotify: Boolean
|
||||
): Result<NotifiableEvent> = runCatchingExceptions {
|
||||
val content = notificationData.content as? NotificationContent.MessageLike.CallNotify
|
||||
?: throw ResolvingException("content is not a call notify")
|
||||
?: throw NotificationResolverException.UnknownError("content is not a call notify")
|
||||
|
||||
val previousRingingCallStatus = appForegroundStateService.hasRingingCall.value
|
||||
// We need the sync service working to get the updated room info
|
||||
|
|
@ -65,8 +66,12 @@ class DefaultCallNotificationEventResolver @Inject constructor(
|
|||
if (content.type == CallNotifyType.RING) {
|
||||
appForegroundStateService.updateHasRingingCall(true)
|
||||
|
||||
val client = clientProvider.getOrRestore(sessionId).getOrNull() ?: throw ResolvingException("Session $sessionId not found")
|
||||
val room = client.getRoom(notificationData.roomId) ?: throw ResolvingException("Room ${notificationData.roomId} not found")
|
||||
val client = clientProvider.getOrRestore(
|
||||
sessionId
|
||||
).getOrNull() ?: throw NotificationResolverException.UnknownError("Session $sessionId not found")
|
||||
val room = client.getRoom(
|
||||
notificationData.roomId
|
||||
) ?: throw NotificationResolverException.UnknownError("Room ${notificationData.roomId} not found")
|
||||
// Give a few seconds for the room info flow to catch up with the sync, if needed - this is usually instant
|
||||
val isActive = withTimeoutOrNull(3.seconds) { room.roomInfoFlow.firstOrNull { it.hasRoomCall } }?.hasRoomCall ?: false
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +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.mapCatchingExceptions
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
||||
import io.element.android.libraries.di.AppScope
|
||||
|
|
@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
|
|||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
||||
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||
import io.element.android.libraries.matrix.api.media.getMediaPreviewValue
|
||||
import io.element.android.libraries.matrix.api.notification.NotificationContent
|
||||
|
|
@ -43,18 +44,24 @@ import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageT
|
|||
import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType
|
||||
import io.element.android.libraries.matrix.ui.messages.toPlainText
|
||||
import io.element.android.libraries.push.impl.R
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private val loggerTag = LoggerTag("DefaultNotifiableEventResolver", LoggerTag.NotificationLoggerTag)
|
||||
|
||||
/**
|
||||
* Result of resolving a batch of push events.
|
||||
* The outermost [Result] indicates whether the setup to resolve the events was successful.
|
||||
* The results for each push notification will be a map of [NotificationEventRequest] to [Result] of [ResolvedPushEvent].
|
||||
* If the resolution of a specific event fails, the innermost [Result] will contain an exception.
|
||||
*/
|
||||
typealias ResolvePushEventsResult = Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>
|
||||
|
||||
/**
|
||||
* The notifiable event resolver is able to create a NotifiableEvent (view model for notifications) from an sdk Event.
|
||||
* It is used as a bridge between the Event Thread and the NotificationDrawerManager.
|
||||
|
|
@ -65,24 +72,24 @@ interface NotifiableEventResolver {
|
|||
suspend fun resolveEvents(
|
||||
sessionId: SessionId,
|
||||
notificationEventRequests: List<NotificationEventRequest>
|
||||
): Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>>
|
||||
): ResolvePushEventsResult
|
||||
}
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@SingleIn(AppScope::class)
|
||||
class DefaultNotifiableEventResolver @Inject constructor(
|
||||
private val stringProvider: StringProvider,
|
||||
private val clock: SystemClock,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
private val notificationMediaRepoFactory: NotificationMediaRepo.Factory,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val permalinkParser: PermalinkParser,
|
||||
private val callNotificationEventResolver: CallNotificationEventResolver,
|
||||
private val fallbackNotificationFactory: FallbackNotificationFactory,
|
||||
) : NotifiableEventResolver {
|
||||
override suspend fun resolveEvents(
|
||||
sessionId: SessionId,
|
||||
notificationEventRequests: List<NotificationEventRequest>
|
||||
): Result<Map<NotificationEventRequest, Result<ResolvedPushEvent>>> {
|
||||
): ResolvePushEventsResult {
|
||||
Timber.d("Queueing notifications: $notificationEventRequests")
|
||||
val client = matrixClientProvider.getOrRestore(sessionId).getOrElse {
|
||||
return Result.failure(IllegalStateException("Couldn't get or restore client for session $sessionId"))
|
||||
|
|
@ -90,20 +97,28 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
val ids = notificationEventRequests.groupBy { it.roomId }.mapValues { (_, value) -> value.map { it.eventId } }
|
||||
|
||||
// TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event
|
||||
val notifications = client.notificationService().getNotifications(ids).mapCatchingExceptions { map ->
|
||||
map.mapValues { (_, notificationData) ->
|
||||
notificationData.asNotifiableEvent(client, sessionId)
|
||||
val notificationsResult = client.notificationService().getNotifications(ids)
|
||||
|
||||
if (notificationsResult.isFailure) {
|
||||
val exception = notificationsResult.exceptionOrNull()
|
||||
Timber.tag(loggerTag.value).e(exception, "Failed to get notifications for $ids")
|
||||
return Result.failure(exception ?: NotificationResolverException.UnknownError("Unknown error while fetching notifications"))
|
||||
}
|
||||
|
||||
// The null check is done above
|
||||
val notificationDataMap = notificationsResult.getOrNull()!!.mapValues { (_, notificationData) ->
|
||||
notificationData.flatMap { data ->
|
||||
data.asNotifiableEvent(client, sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(
|
||||
notificationEventRequests.associate {
|
||||
val notificationData = notifications.getOrNull()?.get(it.eventId)
|
||||
if (notificationData != null) {
|
||||
it to notificationData
|
||||
notificationEventRequests.associate { request ->
|
||||
val notificationDataResult = notificationDataMap[request.eventId]
|
||||
if (notificationDataResult == null) {
|
||||
request to Result.failure(NotificationResolverException.UnknownError("No notification data for ${request.roomId} - ${request.eventId}"))
|
||||
} else {
|
||||
// TODO once the SDK can actually return what went wrong, we should return it here instead of this generic error
|
||||
it to Result.failure(ResolvingException("No notification data for ${it.roomId} - ${it.eventId}"))
|
||||
request to notificationDataResult
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -164,7 +179,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
NotificationContent.MessageLike.CallCandidates,
|
||||
NotificationContent.MessageLike.CallHangup -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||
throw ResolvingException("Ignoring notification for call ${content.javaClass.simpleName}")
|
||||
throw NotificationResolverException.EventFilteredOut
|
||||
}
|
||||
is NotificationContent.MessageLike.CallInvite -> {
|
||||
val notifiableMessageEvent = buildNotifiableMessageEvent(
|
||||
|
|
@ -195,7 +210,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
NotificationContent.MessageLike.KeyVerificationReady,
|
||||
NotificationContent.MessageLike.KeyVerificationStart -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for verification ${content.javaClass.simpleName}")
|
||||
throw ResolvingException("Ignoring notification for verification ${content.javaClass.simpleName}")
|
||||
throw NotificationResolverException.EventFilteredOut
|
||||
}
|
||||
is NotificationContent.MessageLike.Poll -> {
|
||||
val notifiableEventMessage = buildNotifiableMessageEvent(
|
||||
|
|
@ -217,16 +232,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
}
|
||||
is NotificationContent.MessageLike.ReactionContent -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for reaction")
|
||||
throw ResolvingException("Ignoring notification for reaction")
|
||||
throw NotificationResolverException.EventFilteredOut
|
||||
}
|
||||
NotificationContent.MessageLike.RoomEncrypted -> {
|
||||
Timber.tag(loggerTag.value).w("Notification with encrypted content -> fallback")
|
||||
val fallbackNotifiableEvent = fallbackNotifiableEvent(userId, roomId, eventId)
|
||||
ResolvedPushEvent.Event(fallbackNotifiableEvent)
|
||||
}
|
||||
NotificationContent.MessageLike.UnableToResolve -> {
|
||||
Timber.tag(loggerTag.value).w("Unable to resolve notification -> fallback")
|
||||
val fallbackNotifiableEvent = fallbackNotifiableEvent(userId, roomId, eventId)
|
||||
val fallbackNotifiableEvent = fallbackNotificationFactory.create(userId, roomId, eventId)
|
||||
ResolvedPushEvent.Event(fallbackNotifiableEvent)
|
||||
}
|
||||
is NotificationContent.MessageLike.RoomRedaction -> {
|
||||
|
|
@ -234,7 +244,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
val redactedEventId = content.redactedEventId
|
||||
if (redactedEventId == null) {
|
||||
Timber.tag(loggerTag.value).d("redactedEventId is null.")
|
||||
throw ResolvingException("redactedEventId is null")
|
||||
throw NotificationResolverException.UnknownError("redactedEventId is null")
|
||||
} else {
|
||||
ResolvedPushEvent.Redaction(
|
||||
sessionId = userId,
|
||||
|
|
@ -246,7 +256,7 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
}
|
||||
NotificationContent.MessageLike.Sticker -> {
|
||||
Timber.tag(loggerTag.value).d("Ignoring notification for sticker")
|
||||
throw ResolvingException("Ignoring notification for reaction")
|
||||
throw NotificationResolverException.EventFilteredOut
|
||||
}
|
||||
is NotificationContent.StateEvent.RoomMemberContent,
|
||||
NotificationContent.StateEvent.PolicyRuleRoom,
|
||||
|
|
@ -270,27 +280,11 @@ class DefaultNotifiableEventResolver @Inject constructor(
|
|||
NotificationContent.StateEvent.SpaceChild,
|
||||
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}")
|
||||
throw NotificationResolverException.EventFilteredOut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fallbackNotifiableEvent(
|
||||
userId: SessionId,
|
||||
roomId: RoomId,
|
||||
eventId: EventId
|
||||
) = FallbackNotifiableEvent(
|
||||
sessionId = userId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
timestamp = clock.epochMillis(),
|
||||
description = stringProvider.getString(R.string.notification_fallback_content),
|
||||
)
|
||||
|
||||
private fun descriptionFromMessageContent(
|
||||
content: NotificationContent.MessageLike.RoomMessage,
|
||||
senderDisambiguatedDisplayName: String,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor(
|
|||
?.getNotifications(mapOf(roomId to listOf(eventId)))
|
||||
?.getOrNull()
|
||||
?.get(eventId)
|
||||
?.getOrNull()
|
||||
?: return
|
||||
|
||||
val notifiableEvent = callNotificationEventResolver.resolveEvent(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
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.R
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
import io.element.android.services.toolbox.api.strings.StringProvider
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
import javax.inject.Inject
|
||||
|
||||
class FallbackNotificationFactory @Inject constructor(
|
||||
private val clock: SystemClock,
|
||||
private val stringProvider: StringProvider,
|
||||
) {
|
||||
fun create(
|
||||
sessionId: SessionId,
|
||||
roomId: RoomId,
|
||||
eventId: EventId,
|
||||
): FallbackNotifiableEvent = FallbackNotifiableEvent(
|
||||
sessionId = sessionId,
|
||||
roomId = roomId,
|
||||
eventId = eventId,
|
||||
editedEventId = null,
|
||||
canBeReplaced = true,
|
||||
isRedacted = false,
|
||||
isUpdated = false,
|
||||
timestamp = clock.epochMillis(),
|
||||
description = stringProvider.getString(R.string.notification_fallback_content),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/*
|
||||
* 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)
|
||||
|
|
@ -16,16 +16,17 @@ import io.element.android.libraries.di.AppScope
|
|||
import io.element.android.libraries.di.SingleIn
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.api.exception.NotificationResolverException
|
||||
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.FallbackNotificationFactory
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationEventRequest
|
||||
import io.element.android.libraries.push.impl.notifications.NotificationResolverQueue
|
||||
import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels
|
||||
import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
|
||||
import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent
|
||||
|
|
@ -63,6 +64,7 @@ class DefaultPushHandler @Inject constructor(
|
|||
private val resolverQueue: NotificationResolverQueue,
|
||||
@AppCoroutineScope
|
||||
private val appCoroutineScope: CoroutineScope,
|
||||
private val fallbackNotificationFactory: FallbackNotificationFactory,
|
||||
) : PushHandler {
|
||||
init {
|
||||
processPushEventResults()
|
||||
|
|
@ -88,34 +90,37 @@ class DefaultPushHandler @Inject constructor(
|
|||
} else {
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
if (it is ResolvedPushEvent.Event && it.notifiableEvent is FallbackNotifiableEvent) {
|
||||
pushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo = request.providerInfo,
|
||||
eventId = request.eventId,
|
||||
roomId = request.roomId,
|
||||
sessionId = request.sessionId,
|
||||
reason = "Showing fallback notification",
|
||||
)
|
||||
mutableBatteryOptimizationStore.showBatteryOptimizationBanner()
|
||||
} else {
|
||||
pushHistoryService.onSuccess(
|
||||
providerInfo = request.providerInfo,
|
||||
eventId = request.eventId,
|
||||
roomId = request.roomId,
|
||||
sessionId = request.sessionId,
|
||||
comment = "Push handled successfully",
|
||||
)
|
||||
},
|
||||
onFailure = { exception ->
|
||||
if (exception is NotificationResolverException.EventFilteredOut) {
|
||||
pushHistoryService.onSuccess(
|
||||
providerInfo = request.providerInfo,
|
||||
eventId = request.eventId,
|
||||
roomId = request.roomId,
|
||||
sessionId = request.sessionId,
|
||||
comment = "Push handled successfully",
|
||||
comment = "Push handled successfully but notification was filtered out",
|
||||
)
|
||||
} else {
|
||||
val reason = when (exception) {
|
||||
is NotificationResolverException.EventNotFound -> "Event not found"
|
||||
else -> "Unknown error: ${exception.message}"
|
||||
}
|
||||
pushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo = request.providerInfo,
|
||||
eventId = request.eventId,
|
||||
roomId = request.roomId,
|
||||
sessionId = request.sessionId,
|
||||
reason = "$reason - Showing fallback notification",
|
||||
)
|
||||
mutableBatteryOptimizationStore.showBatteryOptimizationBanner()
|
||||
}
|
||||
},
|
||||
onFailure = { exception ->
|
||||
pushHistoryService.onUnableToResolveEvent(
|
||||
providerInfo = request.providerInfo,
|
||||
eventId = request.eventId,
|
||||
roomId = request.roomId,
|
||||
sessionId = request.sessionId,
|
||||
reason = exception.message ?: exception.javaClass.simpleName,
|
||||
)
|
||||
mutableBatteryOptimizationStore.showBatteryOptimizationBanner()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -125,8 +130,21 @@ class DefaultPushHandler @Inject constructor(
|
|||
val redactions = mutableListOf<ResolvedPushEvent.Redaction>()
|
||||
|
||||
@Suppress("LoopWithTooManyJumpStatements")
|
||||
for (result in resolvedEvents.values) {
|
||||
val event = result.getOrNull() ?: continue
|
||||
for ((request, result) in resolvedEvents) {
|
||||
val event = result.recover { exception ->
|
||||
// If the event could not be resolved, we create a fallback notification
|
||||
when (exception) {
|
||||
is NotificationResolverException.EventFilteredOut -> {
|
||||
// Do nothing, we don't want to show a notification for filtered out events
|
||||
null
|
||||
}
|
||||
else -> {
|
||||
Timber.tag(loggerTag.value).e(exception, "Failed to resolve push event")
|
||||
ResolvedPushEvent.Event(fallbackNotificationFactory.create(request.sessionId, request.roomId, request.eventId))
|
||||
}
|
||||
}
|
||||
}.getOrNull() ?: continue
|
||||
|
||||
val userPushStore = userPushStoreFactory.getOrCreate(event.sessionId)
|
||||
val areNotificationsEnabled = userPushStore.getNotificationEnabledForDevice().first()
|
||||
// If notifications are disabled for this session and device, we don't want to show the notification
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue