From 04a1c00b94cb3eae3a77199ca6a2217023bee38c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 17:56:51 +0200 Subject: [PATCH] Update dependency org.matrix.rustcomponents:sdk-android to v25.7.7 (#4989) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- gradle/libs.versions.toml | 2 +- .../NotificationResolverException.kt | 28 ++++ .../api/notification/NotificationData.kt | 1 - .../api/notification/NotificationService.kt | 15 +- .../notification/RustNotificationService.kt | 74 ++++++---- .../fixtures/factories/NotificationItem.kt | 7 + .../fakes/FakeFfiNotificationClient.kt | 6 +- .../RustNotificationServiceTest.kt | 13 +- .../notification/FakeNotificationService.kt | 6 +- .../CallNotificationEventResolver.kt | 11 +- .../DefaultNotifiableEventResolver.kt | 80 +++++----- .../DefaultOnMissedCallNotificationHandler.kt | 1 + .../FallbackNotificationFactory.kt | 38 +++++ .../impl/notifications/ResolvingException.kt | 10 -- .../push/impl/push/DefaultPushHandler.kt | 66 ++++++--- .../DefaultNotifiableEventResolverTest.kt | 138 +++++++++--------- ...aultOnMissedCallNotificationHandlerTest.kt | 2 +- .../push/impl/push/DefaultPushHandlerTest.kt | 15 +- 18 files changed, 313 insertions(+), 200 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/NotificationResolverException.kt create mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt delete mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ResolvingException.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77e212c2ec..e882f11531 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -173,7 +173,7 @@ jsoup = "org.jsoup:jsoup:1.21.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.1.0" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.7.3" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.7.7" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/NotificationResolverException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/NotificationResolverException.kt new file mode 100644 index 0000000000..ab251f19d8 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/NotificationResolverException.kt @@ -0,0 +1,28 @@ +/* + * 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.matrix.api.exception + +/** + * Exceptions that can occur while resolving the events associated to push notifications. + */ +sealed class NotificationResolverException : Exception() { + /** + * The event was not found by the notification service. + */ + data object EventNotFound : NotificationResolverException() + + /** + * The event was found but it was filtered out by the notification service. + */ + data object EventFilteredOut : NotificationResolverException() + + /** + * An unexpected error occurred while trying to resolve the event. + */ + data class UnknownError(override val message: String) : NotificationResolverException() +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index f90b70171e..338193ed44 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -68,7 +68,6 @@ sealed interface NotificationContent { ) : MessageLike data object RoomEncrypted : MessageLike - data object UnableToResolve : MessageLike data class RoomMessage( val senderId: UserId, val messageType: MessageType diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt index 1e1c8b7fb7..ddec326cef 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationService.kt @@ -10,6 +10,19 @@ package io.element.android.libraries.matrix.api.notification import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +/** + * Represents the resolution state of an attempt to retrieve notification data for a set of event ids. + * The outer [Result] indicates the success or failure of the setup to retrieve notifications. + * The inner [Result] for each [EventId] in the map indicates whether the notification data was successfully retrieved or if there was an error. + */ +typealias GetNotificationDataResult = Result>> + +/** + * Service to retrieve notifications for a given set of event ids in specific rooms. + */ interface NotificationService { - suspend fun getNotifications(ids: Map>): Result> + /** + * Fetch notifications for the specified event ids in the given rooms. + */ + suspend fun getNotifications(ids: Map>): GetNotificationDataResult } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt index b88b53454a..4b4693c73b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt @@ -12,26 +12,29 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions 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.matrix.api.notification.NotificationContent -import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.exception.NotificationResolverException +import io.element.android.libraries.matrix.api.notification.GetNotificationDataResult import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.withContext +import org.matrix.rustcomponents.sdk.BatchNotificationResult import org.matrix.rustcomponents.sdk.NotificationClient import org.matrix.rustcomponents.sdk.NotificationItemsRequest +import org.matrix.rustcomponents.sdk.NotificationStatus +import org.matrix.rustcomponents.sdk.use import timber.log.Timber class RustNotificationService( private val sessionId: SessionId, private val notificationClient: NotificationClient, private val dispatchers: CoroutineDispatchers, - private val clock: SystemClock, + clock: SystemClock, ) : NotificationService { private val notificationMapper: NotificationMapper = NotificationMapper(clock) override suspend fun getNotifications( ids: Map> - ): Result> = withContext(dispatchers.io) { + ): GetNotificationDataResult = withContext(dispatchers.io) { runCatchingExceptions { val requests = ids.map { (roomId, eventIds) -> NotificationItemsRequest( @@ -42,34 +45,41 @@ class RustNotificationService( val items = notificationClient.getNotifications(requests) buildMap { val eventIds = requests.flatMap { it.eventIds } - for (eventId in eventIds) { - val item = items[eventId] - val roomId = RoomId(requests.find { it.eventIds.contains(eventId) }?.roomId!!) - if (item != null) { - put(EventId(eventId), notificationMapper.map(sessionId, EventId(eventId), roomId, item)) - } else { - Timber.e("Could not retrieve event for notification with $eventId") - put( - EventId(eventId), - NotificationData( - sessionId = sessionId, - eventId = EventId(eventId), - threadId = null, - roomId = roomId, - senderAvatarUrl = null, - senderDisplayName = null, - senderIsNameAmbiguous = false, - roomAvatarUrl = null, - roomDisplayName = null, - isDirect = false, - isDm = false, - isEncrypted = false, - isNoisy = false, - timestamp = clock.epochMillis(), - content = NotificationContent.MessageLike.UnableToResolve, - hasMention = false - ) - ) + for (rawEventId in eventIds) { + val roomId = RoomId(requests.find { it.eventIds.contains(rawEventId) }?.roomId!!) + val eventId = EventId(rawEventId) + items[rawEventId].use { result -> + when (result) { + is BatchNotificationResult.Ok -> { + when (val status = result.status) { + is NotificationStatus.Event -> { + put(eventId, Result.success(notificationMapper.map(sessionId, eventId, roomId, status.item))) + } + is NotificationStatus.EventNotFound -> { + Timber.e("Could not retrieve event for notification with $eventId - event not found") + put(eventId, Result.failure(NotificationResolverException.EventNotFound)) + } + is NotificationStatus.EventFilteredOut -> { + Timber.d("Could not retrieve event for notification with $eventId - event filtered out") + put(eventId, Result.failure(NotificationResolverException.EventFilteredOut)) + } + } + } + is BatchNotificationResult.Error -> { + Timber.e("Error while retrieving notification with $rawEventId - ${result.message}") + put( + eventId, + Result.failure(NotificationResolverException.UnknownError(result.message)) + ) + } + null -> { + Timber.e("The notification data for $rawEventId was not in the retrieved results. This is unexpected.") + put( + eventId, + Result.failure(NotificationResolverException.UnknownError("Notification data not found")) + ) + } + } } } } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt index b13e48e669..ee5e3ac4e2 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/factories/NotificationItem.kt @@ -16,6 +16,7 @@ import org.matrix.rustcomponents.sdk.NotificationEvent import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.NotificationRoomInfo import org.matrix.rustcomponents.sdk.NotificationSenderInfo +import org.matrix.rustcomponents.sdk.NotificationStatus import org.matrix.rustcomponents.sdk.TimelineEvent fun aRustNotificationItem( @@ -34,6 +35,12 @@ fun aRustNotificationItem( threadId = threadId?.value, ) +fun aRustBatchNotificationResult( + notificationStatus: NotificationStatus = NotificationStatus.Event(aRustNotificationItem()), +) = org.matrix.rustcomponents.sdk.BatchNotificationResult.Ok( + status = notificationStatus, +) + fun aRustNotificationSenderInfo( displayName: String? = A_USER_NAME, avatarUrl: String? = null, diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt index 180da9cc89..d17f4f949c 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiNotificationClient.kt @@ -7,16 +7,16 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes +import org.matrix.rustcomponents.sdk.BatchNotificationResult import org.matrix.rustcomponents.sdk.NoPointer import org.matrix.rustcomponents.sdk.NotificationClient -import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.NotificationItemsRequest class FakeFfiNotificationClient( - var notificationItemResult: Map = emptyMap(), + var notificationItemResult: Map = emptyMap(), val closeResult: () -> Unit = { } ) : NotificationClient(NoPointer) { - override suspend fun getNotifications(requests: List): Map { + override suspend fun getNotifications(requests: List): Map { return notificationItemResult } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt index 3166acc891..117d164ab9 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationServiceTest.kt @@ -8,9 +8,10 @@ package io.element.android.libraries.matrix.impl.notification import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.exception.NotificationResolverException import io.element.android.libraries.matrix.api.notification.NotificationContent import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType -import io.element.android.libraries.matrix.impl.fixtures.factories.aRustNotificationItem +import io.element.android.libraries.matrix.impl.fixtures.factories.aRustBatchNotificationResult import io.element.android.libraries.matrix.impl.fixtures.fakes.FakeFfiNotificationClient import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_MESSAGE @@ -30,12 +31,12 @@ class RustNotificationServiceTest { @Test fun test() = runTest { val notificationClient = FakeFfiNotificationClient( - notificationItemResult = mapOf(AN_EVENT_ID.value to aRustNotificationItem()), + notificationItemResult = mapOf(AN_EVENT_ID.value to aRustBatchNotificationResult()), ) val sut = createRustNotificationService( notificationClient = notificationClient, ) - val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!! + val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!!.getOrThrow() assertThat(result.isEncrypted).isTrue() assertThat(result.content).isEqualTo( NotificationContent.MessageLike.RoomMessage( @@ -56,10 +57,8 @@ class RustNotificationServiceTest { val sut = createRustNotificationService( notificationClient = notificationClient, ) - val result = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!! - assertThat(result.content).isEqualTo( - NotificationContent.MessageLike.UnableToResolve - ) + val exception = sut.getNotifications(mapOf(A_ROOM_ID to listOf(AN_EVENT_ID))).getOrThrow()[AN_EVENT_ID]!!.exceptionOrNull() + assertThat(exception).isInstanceOf(NotificationResolverException::class.java) } @Test diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt index 4a9671f677..4e17265f7a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/FakeNotificationService.kt @@ -13,13 +13,13 @@ import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationService class FakeNotificationService : NotificationService { - private var getNotificationsResult: Result> = Result.success(emptyMap()) + private var getNotificationsResult: Result>> = Result.success(emptyMap()) - fun givenGetNotificationsResult(result: Result>) { + fun givenGetNotificationsResult(result: Result>>) { getNotificationsResult = result } - override suspend fun getNotifications(ids: Map>): Result> { + override suspend fun getNotifications(ids: Map>): Result>> { return getNotificationsResult } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt index 119eda3423..4dcb2debc0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/CallNotificationEventResolver.kt @@ -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 = 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 diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt index 7e7dbe3fb4..c0fca17d81 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolver.kt @@ -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>> + /** * 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 - ): Result>> + ): 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 - ): Result>> { + ): 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, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt index 1efe57ce06..d65df65693 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandler.kt @@ -33,6 +33,7 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor( ?.getNotifications(mapOf(roomId to listOf(eventId))) ?.getOrNull() ?.get(eventId) + ?.getOrNull() ?: return val notifiableEvent = callNotificationEventResolver.resolveEvent( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt new file mode 100644 index 0000000000..947e62bf75 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactory.kt @@ -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), + ) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ResolvingException.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ResolvingException.kt deleted file mode 100644 index 11c655ec1c..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/ResolvingException.kt +++ /dev/null @@ -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) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 4d9e390690..dc2e7bab0a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -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() @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 diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt index 3c609f184e..a6e443b403 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotifiableEventResolverTest.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.exception.NotificationResolverException import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.notification.CallNotifyType import io.element.android.libraries.matrix.api.notification.NotificationContent @@ -51,6 +52,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableMess import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.test.notifications.FakeCallNotificationEventResolver import io.element.android.services.toolbox.impl.strings.AndroidStringProvider +import io.element.android.services.toolbox.test.strings.FakeStringProvider import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import kotlinx.coroutines.test.runTest @@ -71,12 +73,22 @@ class DefaultNotifiableEventResolverTest { } @Test - fun `resolve event failure`() = runTest { + fun `resolve fetching failure`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.failure(AN_EXCEPTION) ) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) + assertThat(result.isFailure).isTrue() + } + + @Test + fun `resolve event failure`() = runTest { + val sut = createDefaultNotifiableEventResolver( + notificationResult = Result.success(mapOf(AN_EVENT_ID to Result.failure(AN_EXCEPTION))) + ) + val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") + val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) assertThat(result.getEvent(request)?.isFailure).isTrue() } @@ -85,12 +97,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = TextMessageType(body = "Hello world", formatted = null) ), - ) + )) ) ) ) @@ -108,13 +120,13 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = TextMessageType(body = "Hello world", formatted = null) ), hasMention = true, - ) + )) ) ) ) @@ -131,7 +143,7 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = TextMessageType( @@ -142,7 +154,7 @@ class DefaultNotifiableEventResolverTest { ) ) ), - ) + )) ) ) ) @@ -159,7 +171,7 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = TextMessageType( @@ -170,7 +182,7 @@ class DefaultNotifiableEventResolverTest { ) ) ), - ) + )) ) ) ) @@ -187,12 +199,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = AudioMessageType("Audio", null, null, MediaSource("url"), null) ), - ) + )) ) ) ) @@ -209,12 +221,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = VideoMessageType("Video", null, null, MediaSource("url"), null) ), - ) + )) ) ) ) @@ -231,12 +243,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = VoiceMessageType("Voice", null, null, MediaSource("url"), null, null) ), - ) + )) ) ) ) @@ -253,12 +265,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = ImageMessageType("Image", null, null, MediaSource("url"), null), ), - ) + )) ) ) ) @@ -275,12 +287,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = StickerMessageType("Sticker", null, null, MediaSource("url"), null), ), - ) + )) ) ) ) @@ -297,12 +309,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = FileMessageType("File", null, null, MediaSource("url"), null), ), - ) + )) ) ) ) @@ -319,12 +331,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = LocationMessageType("Location", "geo:1,2", null), ), - ) + )) ) ) ) @@ -341,12 +353,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = NoticeMessageType("Notice", null), ), - ) + )) ) ) ) @@ -363,12 +375,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomMessage( senderId = A_USER_ID_2, messageType = EmoteMessageType("is happy", null), ), - ) + )) ) ) ) @@ -385,12 +397,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.Poll( senderId = A_USER_ID_2, question = "A question" ), - ) + )) ) ) ) @@ -407,13 +419,13 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.StateEvent.RoomMemberContent( userId = A_USER_ID_2, membershipState = RoomMembershipState.INVITE ), isDirect = false, - ) + )) ) ) ) @@ -427,12 +439,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.Invite( senderId = A_USER_ID_2, ), isDirect = false, - ) + )) ) ) ) @@ -464,12 +476,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.Invite( senderId = A_USER_ID_2, ), isDirect = true, - ) + )) ) ) ) @@ -501,13 +513,13 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.Invite( senderId = A_USER_ID_2, ), isDirect = true, senderDisplayName = null, - ) + )) ) ) ) @@ -539,7 +551,8 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success( + aNotificationData( content = NotificationContent.Invite( senderId = A_USER_ID_2, ), @@ -547,6 +560,7 @@ class DefaultNotifiableEventResolverTest { senderIsNameAmbiguous = true, ) ) + ) ) ) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") @@ -577,12 +591,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.StateEvent.RoomMemberContent( userId = A_USER_ID_2, membershipState = RoomMembershipState.JOIN ) - ) + )) ) ) ) @@ -595,7 +609,7 @@ class DefaultNotifiableEventResolverTest { fun `resolve RoomEncrypted`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - mapOf(AN_EVENT_ID to aNotificationData(content = NotificationContent.MessageLike.RoomEncrypted)) + mapOf(AN_EVENT_ID to Result.success(aNotificationData(content = NotificationContent.MessageLike.RoomEncrypted))) ) ) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") @@ -620,25 +634,12 @@ class DefaultNotifiableEventResolverTest { fun `resolve UnableToResolve`() = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - mapOf(AN_EVENT_ID to aNotificationData(content = NotificationContent.MessageLike.UnableToResolve)) + mapOf(AN_EVENT_ID to Result.failure(NotificationResolverException.EventNotFound)) ) ) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") val result = sut.resolveEvents(A_SESSION_ID, listOf(request)) - val expectedResult = ResolvedPushEvent.Event( - FallbackNotifiableEvent( - sessionId = A_SESSION_ID, - roomId = A_ROOM_ID, - eventId = AN_EVENT_ID, - editedEventId = null, - description = "You have new messages.", - canBeReplaced = true, - isRedacted = false, - isUpdated = false, - timestamp = A_FAKE_TIMESTAMP, - ) - ) - assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) + assertThat(result.getEvent(request)).isEqualTo(Result.failure(NotificationResolverException.EventNotFound)) } @Test @@ -646,10 +647,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success( + aNotificationData( content = NotificationContent.MessageLike.CallInvite(A_USER_ID_2), ) ) + ) ) ) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") @@ -688,12 +691,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.CallNotify( A_USER_ID_2, CallNotifyType.NOTIFY ), - ) + )) ) ), callNotificationEventResolver = callNotificationEventResolver, @@ -729,12 +732,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomRedaction( AN_EVENT_ID_2, A_REDACTION_REASON, ) - ) + )) ) ) ) @@ -754,12 +757,12 @@ class DefaultNotifiableEventResolverTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( mapOf( - AN_EVENT_ID to aNotificationData( + AN_EVENT_ID to Result.success(aNotificationData( content = NotificationContent.MessageLike.RoomRedaction( null, A_REDACTION_REASON, ) - ) + )) ) ) ) @@ -807,7 +810,7 @@ class DefaultNotifiableEventResolverTest { private fun testNoResults(content: NotificationContent) = runTest { val sut = createDefaultNotifiableEventResolver( notificationResult = Result.success( - mapOf(AN_EVENT_ID to aNotificationData(content = content)) + mapOf(AN_EVENT_ID to Result.success(aNotificationData(content = content))) ) ) val request = NotificationEventRequest(A_SESSION_ID, A_ROOM_ID, AN_EVENT_ID, "firebase") @@ -823,7 +826,7 @@ class DefaultNotifiableEventResolverTest { private fun createDefaultNotifiableEventResolver( notificationService: FakeNotificationService? = FakeNotificationService(), - notificationResult: Result> = Result.success(emptyMap()), + notificationResult: Result>> = Result.success(emptyMap()), callNotificationEventResolver: FakeCallNotificationEventResolver = FakeCallNotificationEventResolver(), ): DefaultNotifiableEventResolver { val context = RuntimeEnvironment.getApplication() as Context @@ -840,12 +843,15 @@ class DefaultNotifiableEventResolverTest { } return DefaultNotifiableEventResolver( stringProvider = AndroidStringProvider(context.resources), - clock = FakeSystemClock(), matrixClientProvider = matrixClientProvider, notificationMediaRepoFactory = notificationMediaRepoFactory, context = context, permalinkParser = FakePermalinkParser(), callNotificationEventResolver = callNotificationEventResolver, + fallbackNotificationFactory = FallbackNotificationFactory( + clock = FakeSystemClock(), + stringProvider = FakeStringProvider(defaultResult = "You have new messages.") + ) ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt index 1dc21a7ebe..25e00f7977 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultOnMissedCallNotificationHandlerTest.kt @@ -43,7 +43,7 @@ class DefaultOnMissedCallNotificationHandlerTest { val matrixClientProvider = FakeMatrixClientProvider(getClient = { val notificationService = FakeNotificationService().apply { givenGetNotificationsResult( - Result.success(mapOf(AN_EVENT_ID to aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false))) + Result.success(mapOf(AN_EVENT_ID to Result.success(aNotificationData(senderDisplayName = A_USER_NAME, senderIsNameAmbiguous = false)))) ) } Result.success(FakeMatrixClient(notificationService = notificationService)) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 64a3b87aa7..6ec5accd02 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -18,6 +18,7 @@ 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.matrix.api.core.UserId +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.timeline.item.event.EventType import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -31,9 +32,9 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.push.impl.history.FakePushHistoryService import io.element.android.libraries.push.impl.history.PushHistoryService import io.element.android.libraries.push.impl.notifications.FakeNotifiableEventResolver +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.ResolvingException import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableCallEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent @@ -47,6 +48,8 @@ import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStore import io.element.android.libraries.pushstore.test.userpushstore.FakeUserPushStoreFactory import io.element.android.libraries.pushstore.test.userpushstore.clientsecret.FakePushClientSecret +import io.element.android.services.toolbox.test.strings.FakeStringProvider +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -271,7 +274,7 @@ class DefaultPushHandlerTest { fun `when classical PushData is received, but a failure occurs (session not found), nothing happen`() { `test notification resolver failure`( notificationResolveResult = { _ -> - Result.failure(ResolvingException("Unable to restore session")) + Result.failure(NotificationResolverException.UnknownError("Unable to restore session")) }, shouldSetOptimizationBatteryBanner = false, ) @@ -282,7 +285,7 @@ class DefaultPushHandlerTest { `test notification resolver failure`( notificationResolveResult = { requests: List -> Result.success( - requests.associateWith { Result.failure(ResolvingException("Unable to resolve event")) } + requests.associateWith { Result.failure(NotificationResolverException.UnknownError("Unable to resolve event")) } ) }, shouldSetOptimizationBatteryBanner = true, @@ -336,8 +339,6 @@ class DefaultPushHandlerTest { notifiableEventResult.assertions() .isCalledOnce() .with(value(A_USER_ID), any()) - onNotifiableEventsReceived.assertions() - .isNeverCalled() onPushReceivedResult.assertions() .isCalledOnce() .with(any(), value(AN_EVENT_ID), value(A_ROOM_ID), value(A_USER_ID), value(false), value(true), any()) @@ -662,6 +663,10 @@ class DefaultPushHandlerTest { pushHistoryService = pushHistoryService, resolverQueue = NotificationResolverQueue(notifiableEventResolver = FakeNotifiableEventResolver(notifiableEventsResult), backgroundScope), appCoroutineScope = backgroundScope, + fallbackNotificationFactory = FallbackNotificationFactory( + clock = FakeSystemClock(), + stringProvider = FakeStringProvider(), + ) ) } }