From 5852558681ecaf1ebac5c672983ae32162945bed Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Jun 2025 16:00:31 +0200 Subject: [PATCH 01/10] Notification: ensure that a notification is displayed when a Push cannot be resolved. Previously the error was logged and added to push history but no notification was shown, so the user fully miss the new message. --- .../api/notification/NotificationData.kt | 1 + .../notification/RustNotificationService.kt | 26 +++++++++++++++++-- .../DefaultNotifiableEventResolver.kt | 5 ++++ .../push/impl/push/DefaultPushHandler.kt | 26 ++++++++++++++----- 4 files changed, 49 insertions(+), 9 deletions(-) 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 338193ed44..f90b70171e 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,6 +68,7 @@ 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/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 bab7cdea02..b88b53454a 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,6 +12,7 @@ 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.notification.NotificationService import io.element.android.services.toolbox.api.systemclock.SystemClock @@ -24,7 +25,7 @@ class RustNotificationService( private val sessionId: SessionId, private val notificationClient: NotificationClient, private val dispatchers: CoroutineDispatchers, - clock: SystemClock, + private val clock: SystemClock, ) : NotificationService { private val notificationMapper: NotificationMapper = NotificationMapper(clock) @@ -43,11 +44,32 @@ class RustNotificationService( 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) { - val roomId = RoomId(requests.find { it.eventIds.contains(eventId) }?.roomId!!) 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 + ) + ) } } } 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 d5eef83564..9a6b119a26 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 @@ -225,6 +225,11 @@ class DefaultNotifiableEventResolver @Inject constructor( 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) + ResolvedPushEvent.Event(fallbackNotifiableEvent) + } is NotificationContent.MessageLike.RoomRedaction -> { // Note: this case will be handled below val redactedEventId = content.redactedEventId 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 d42ca28c88..70ad155c61 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 @@ -25,6 +25,7 @@ import io.element.android.libraries.push.impl.history.onUnableToRetrieveSession 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 @@ -87,13 +88,24 @@ class DefaultPushHandler @Inject constructor( } else { result.fold( onSuccess = { - pushHistoryService.onSuccess( - providerInfo = request.providerInfo, - eventId = request.eventId, - roomId = request.roomId, - sessionId = request.sessionId, - comment = "Push handled successfully", - ) + if (it is ResolvedPushEvent.Event && it.notifiableEvent is FallbackNotifiableEvent) { + pushHistoryService.onUnableToResolveEvent( + providerInfo = request.providerInfo, + eventId = request.eventId, + roomId = request.roomId, + sessionId = request.sessionId, + reason = "Unable to resolve event, 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 -> pushHistoryService.onUnableToResolveEvent( From c61b118f42193a0c9ec1f37670795f89a7c96f5a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Jun 2025 16:15:49 +0200 Subject: [PATCH 02/10] Update wording of `notification_fallback_content` (from localazy) --- libraries/push/impl/src/main/res/values/localazy.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 9860c8bef7..3155d87487 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -13,7 +13,7 @@ "%d notification" "%d notifications" - "Notification" + "You have new message(s)." "📹 Incoming call" "** Failed to send - please open room" "Join" From d8095faa432de68bc18c4974079a522e807a8abb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Jun 2025 16:26:33 +0200 Subject: [PATCH 03/10] Ensure that the battery optimization banner is not displayed after an internal clear cache. --- .../preferences/impl/tasks/ClearCacheUseCase.kt | 1 + .../impl/tasks/DefaultClearCacheUseCaseTest.kt | 5 ++++- .../android/libraries/push/api/PushService.kt | 5 +++++ .../libraries/push/impl/DefaultPushService.kt | 6 ++++++ .../impl/push/MutableBatteryOptimizationStore.kt | 5 +++++ .../push/impl/DefaultPushServiceTest.kt | 16 ++++++++++++++++ .../push/FakeMutableBatteryOptimizationStore.kt | 5 +++++ .../libraries/push/test/FakePushService.kt | 5 +++++ 8 files changed, 47 insertions(+), 1 deletion(-) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt index 5662e4fd46..9b90c8ba53 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt @@ -59,6 +59,7 @@ class DefaultClearCacheUseCase @Inject constructor( seenInvitesStore.clear() // Ensure any error will be displayed again pushService.setIgnoreRegistrationError(matrixClient.sessionId, false) + pushService.resetBatteryOptimizationState() // Ensure the app is restarted defaultCacheService.onClearedCache(matrixClient.sessionId) } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt index ab391bfc62..cfdc63984c 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/tasks/DefaultClearCacheUseCaseTest.kt @@ -46,8 +46,10 @@ class DefaultClearCacheUseCaseTest { resetLambda = resetFtueLambda, ) val setIgnoreRegistrationErrorLambda = lambdaRecorder { _, _ -> } + val resetBatteryOptimizationStateResult = lambdaRecorder { } val pushService = FakePushService( - setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda + setIgnoreRegistrationErrorLambda = setIgnoreRegistrationErrorLambda, + resetBatteryOptimizationStateResult = resetBatteryOptimizationStateResult, ) val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID)) assertThat(seenInvitesStore.seenRoomIds().first()).isNotEmpty() @@ -68,6 +70,7 @@ class DefaultClearCacheUseCaseTest { resetFtueLambda.assertions().isCalledOnce() setIgnoreRegistrationErrorLambda.assertions().isCalledOnce() .with(value(matrixClient.sessionId), value(false)) + resetBatteryOptimizationStateResult.assertions().isCalledOnce() assertThat(awaitItem()).isEqualTo(matrixClient.sessionId) assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty() assertThat(activeRoomsHolder.getActiveRoom(A_SESSION_ID)).isNull() diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index f9a0496efb..fb1bd14404 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -67,4 +67,9 @@ interface PushService { * Reset the push history, including the push counter. */ suspend fun resetPushHistory() + + /** + * Reset the battery optimization state. + */ + suspend fun resetBatteryOptimizationState() } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index 498b461110..d628f7153e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -15,6 +15,7 @@ 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.push.MutableBatteryOptimizationStore 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 @@ -37,6 +38,7 @@ class DefaultPushService @Inject constructor( private val sessionObserver: SessionObserver, private val pushClientSecretStore: PushClientSecretStore, private val pushDataStore: PushDataStore, + private val mutableBatteryOptimizationStore: MutableBatteryOptimizationStore, ) : PushService, SessionListener { init { observeSessions() @@ -138,4 +140,8 @@ class DefaultPushService @Inject constructor( override suspend fun resetPushHistory() { pushDataStore.reset() } + + override suspend fun resetBatteryOptimizationState() { + mutableBatteryOptimizationStore.reset() + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt index 1c1d9186f6..14e2cfd97b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/MutableBatteryOptimizationStore.kt @@ -15,6 +15,7 @@ import javax.inject.Inject interface MutableBatteryOptimizationStore { suspend fun showBatteryOptimizationBanner() suspend fun onOptimizationBannerDismissed() + suspend fun reset() } @ContributesBinding(AppScope::class) @@ -28,4 +29,8 @@ class DefaultMutableBatteryOptimizationStore @Inject constructor( override suspend fun onOptimizationBannerDismissed() { defaultPushDataStore.setBatteryOptimizationBannerState(DefaultPushDataStore.BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED) } + + override suspend fun reset() { + defaultPushDataStore.setBatteryOptimizationBannerState(DefaultPushDataStore.BATTERY_OPTIMIZATION_BANNER_STATE_INIT) + } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt index f3333e84f7..bf0dddd788 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt @@ -14,6 +14,8 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.push.api.GetCurrentPushProvider +import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore +import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore import io.element.android.libraries.push.impl.store.InMemoryPushDataStore import io.element.android.libraries.push.impl.store.PushDataStore import io.element.android.libraries.push.impl.test.FakeTestPush @@ -283,6 +285,18 @@ class DefaultPushServiceTest { assertThat(userPushStore.getPushProviderName()).isEqualTo(aPushProvider.name) } + @Test + fun `resetBatteryOptimizationState invokes the store method`() = runTest { + val resetResult = lambdaRecorder { } + val defaultPushService = createDefaultPushService( + mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore( + resetResult = resetResult, + ), + ) + defaultPushService.resetBatteryOptimizationState() + resetResult.assertions().isCalledOnce() + } + private fun createDefaultPushService( testPush: TestPush = FakeTestPush(), userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(), @@ -291,6 +305,7 @@ class DefaultPushServiceTest { sessionObserver: SessionObserver = NoOpSessionObserver(), pushClientSecretStore: PushClientSecretStore = InMemoryPushClientSecretStore(), pushDataStore: PushDataStore = InMemoryPushDataStore(), + mutableBatteryOptimizationStore: MutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore(), ): DefaultPushService { return DefaultPushService( testPush = testPush, @@ -300,6 +315,7 @@ class DefaultPushServiceTest { sessionObserver = sessionObserver, pushClientSecretStore = pushClientSecretStore, pushDataStore = pushDataStore, + mutableBatteryOptimizationStore = mutableBatteryOptimizationStore, ) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt index 9e526debbb..d4d3992f1e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/FakeMutableBatteryOptimizationStore.kt @@ -12,6 +12,7 @@ import io.element.android.tests.testutils.lambda.lambdaError class FakeMutableBatteryOptimizationStore( private val showBatteryOptimizationBannerResult: () -> Unit = { lambdaError() }, private val onOptimizationBannerDismissedResult: () -> Unit = { lambdaError() }, + private val resetResult: () -> Unit = { lambdaError() }, ) : MutableBatteryOptimizationStore { override suspend fun showBatteryOptimizationBanner() { showBatteryOptimizationBannerResult() @@ -20,4 +21,8 @@ class FakeMutableBatteryOptimizationStore( override suspend fun onOptimizationBannerDismissed() { onOptimizationBannerDismissedResult() } + + override suspend fun reset() { + resetResult() + } } diff --git a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt index 5c3c01e8e8..553ac09465 100644 --- a/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt +++ b/libraries/push/test/src/main/kotlin/io/element/android/libraries/push/test/FakePushService.kt @@ -28,6 +28,7 @@ class FakePushService( private val selectPushProviderLambda: suspend (SessionId, PushProvider) -> Unit = { _, _ -> lambdaError() }, private val setIgnoreRegistrationErrorLambda: (SessionId, Boolean) -> Unit = { _, _ -> lambdaError() }, private val resetPushHistoryResult: () -> Unit = { lambdaError() }, + private val resetBatteryOptimizationStateResult: () -> Unit = { lambdaError() }, ) : PushService { override suspend fun getCurrentPushProvider(): PushProvider? { return registeredPushProvider ?: currentPushProvider() @@ -92,4 +93,8 @@ class FakePushService( override suspend fun resetPushHistory() = simulateLongTask { resetPushHistoryResult() } + + override suspend fun resetBatteryOptimizationState() { + resetBatteryOptimizationStateResult() + } } From 9c16f9b161d19240492d2a971b6d69966acb4668 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Jun 2025 16:38:56 +0200 Subject: [PATCH 04/10] Add missing unit test on DefaultPushService --- .../push/impl/DefaultPushServiceTest.kt | 39 +++++++++++++++++++ .../push/impl/store/InMemoryPushDataStore.kt | 4 ++ 2 files changed, 43 insertions(+) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt index bf0dddd788..5ae8ab261e 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/DefaultPushServiceTest.kt @@ -7,13 +7,17 @@ package io.element.android.libraries.push.impl +import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.push.api.GetCurrentPushProvider +import io.element.android.libraries.push.api.history.PushHistoryItem import io.element.android.libraries.push.impl.push.FakeMutableBatteryOptimizationStore import io.element.android.libraries.push.impl.push.MutableBatteryOptimizationStore import io.element.android.libraries.push.impl.store.InMemoryPushDataStore @@ -297,6 +301,41 @@ class DefaultPushServiceTest { resetResult.assertions().isCalledOnce() } + @Test + fun `resetPushHistory invokes the store method`() = runTest { + val resetResult = lambdaRecorder { } + val defaultPushService = createDefaultPushService( + pushDataStore = InMemoryPushDataStore( + resetResult = resetResult + ), + ) + defaultPushService.resetPushHistory() + resetResult.assertions().isCalledOnce() + } + + @Test + fun `getPushHistoryItemsFlow invokes the store method`() = runTest { + val store = InMemoryPushDataStore() + val aPushHistoryItem = PushHistoryItem( + pushDate = 0L, + formattedDate = "formattedDate", + providerInfo = "providerInfo", + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + sessionId = A_SESSION_ID, + hasBeenResolved = false, + comment = null, + ) + val defaultPushService = createDefaultPushService( + pushDataStore = store, + ) + defaultPushService.getPushHistoryItemsFlow().test { + assertThat(awaitItem().isEmpty()).isTrue() + store.emitPushHistoryItems(listOf(aPushHistoryItem)) + assertThat(awaitItem().first()).isEqualTo(aPushHistoryItem) + } + } + private fun createDefaultPushService( testPush: TestPush = FakeTestPush(), userPushStoreFactory: UserPushStoreFactory = FakeUserPushStoreFactory(), diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt index 4f710e09b6..41b0546f33 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/store/InMemoryPushDataStore.kt @@ -31,6 +31,10 @@ class InMemoryPushDataStore( return mutablePushHistoryItemsFlow.asStateFlow() } + suspend fun emitPushHistoryItems(items: List) { + mutablePushHistoryItemsFlow.emit(items) + } + override suspend fun reset() { resetResult() } From 5c105c0b476a49ff2f0e3f5452a4a24d77269ea0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 17 Jun 2025 16:55:03 +0200 Subject: [PATCH 05/10] Remove string duplication --- .../android/libraries/push/impl/push/DefaultPushHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 70ad155c61..4d9e390690 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 @@ -94,7 +94,7 @@ class DefaultPushHandler @Inject constructor( eventId = request.eventId, roomId = request.roomId, sessionId = request.sessionId, - reason = "Unable to resolve event, showing fallback notification", + reason = "Showing fallback notification", ) mutableBatteryOptimizationStore.showBatteryOptimizationBanner() } else { From 5054164379272dd4ff80332f17720daf6deec830 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Jun 2025 14:19:57 +0200 Subject: [PATCH 06/10] Fix test after wording change. --- .../impl/notifications/DefaultNotifiableEventResolverTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2ba00156c7..d9a022c961 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 @@ -608,7 +608,7 @@ class DefaultNotifiableEventResolverTest { roomId = A_ROOM_ID, eventId = AN_EVENT_ID, editedEventId = null, - description = "Notification", + description = "You have new message(s).", canBeReplaced = true, isRedacted = false, isUpdated = false, From af68176dbe88017ec33e5be16ae7f664f3a7cb78 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Jun 2025 14:27:28 +0200 Subject: [PATCH 07/10] Add missing test for UnableToResolve case. --- .../DefaultNotifiableEventResolverTest.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) 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 d9a022c961..65342a3b72 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 @@ -618,6 +618,31 @@ class DefaultNotifiableEventResolverTest { assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) } + @Test + fun `resolve UnableToResolve`() = runTest { + val sut = createDefaultNotifiableEventResolver( + notificationResult = Result.success( + mapOf(AN_EVENT_ID to aNotificationData(content = NotificationContent.MessageLike.UnableToResolve)) + ) + ) + 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 message(s).", + canBeReplaced = true, + isRedacted = false, + isUpdated = false, + timestamp = A_FAKE_TIMESTAMP, + ) + ) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) + } + @Test fun `resolve CallInvite`() = runTest { val sut = createDefaultNotifiableEventResolver( From fde4fe871f4709f8512accfe4c4994e7efd7ea02 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Jun 2025 14:30:44 +0200 Subject: [PATCH 08/10] Add test for UnableToResolve case. --- .../notification/RustNotificationServiceTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 c0d7ddf09b..0349523d11 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 @@ -47,6 +47,20 @@ class RustNotificationServiceTest { ) } + @Test + fun `test unable to resolve event`() = runTest { + val notificationClient = FakeFfiNotificationClient( + notificationItemResult = emptyMap(), + ) + 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 + ) + } + private fun TestScope.createRustNotificationService( notificationClient: NotificationClient = FakeFfiNotificationClient(), clock: SystemClock = FakeSystemClock(), From 0a50c9f9cfeff74a3d9f294f0c5c4ccaea461ce4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Jun 2025 14:38:33 +0200 Subject: [PATCH 09/10] Add missing test on RustNotificationService --- .../fixtures/fakes/FakeFfiNotificationClient.kt | 3 +++ .../notification/RustNotificationServiceTest.kt | 14 ++++++++++++++ 2 files changed, 17 insertions(+) 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 ec244ede74..180da9cc89 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 @@ -14,8 +14,11 @@ import org.matrix.rustcomponents.sdk.NotificationItemsRequest class FakeFfiNotificationClient( var notificationItemResult: Map = emptyMap(), + val closeResult: () -> Unit = { } ) : NotificationClient(NoPointer) { override suspend fun getNotifications(requests: List): Map { return notificationItemResult } + + override fun close() = closeResult() } 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 0349523d11..3166acc891 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 @@ -19,6 +19,7 @@ import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -61,6 +62,19 @@ class RustNotificationServiceTest { ) } + @Test + fun `close should invoke the close method of the service`() = runTest { + val closeResult = lambdaRecorder { } + val notificationClient = FakeFfiNotificationClient( + closeResult = closeResult, + ) + val sut = createRustNotificationService( + notificationClient = notificationClient, + ) + sut.close() + closeResult.assertions().isCalledOnce() + } + private fun TestScope.createRustNotificationService( notificationClient: NotificationClient = FakeFfiNotificationClient(), clock: SystemClock = FakeSystemClock(), From 91279d6f0aa92d1ec3539efecdc3c8f3c9b0efcc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 Jun 2025 18:00:50 +0200 Subject: [PATCH 10/10] Update wording of generic notification. --- libraries/push/impl/src/main/res/values/localazy.xml | 2 +- .../impl/notifications/DefaultNotifiableEventResolverTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 3155d87487..069183a3a2 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -13,7 +13,7 @@ "%d notification" "%d notifications" - "You have new message(s)." + "You have new messages." "📹 Incoming call" "** Failed to send - please open room" "Join" 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 65342a3b72..5b93e90064 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 @@ -608,7 +608,7 @@ class DefaultNotifiableEventResolverTest { roomId = A_ROOM_ID, eventId = AN_EVENT_ID, editedEventId = null, - description = "You have new message(s).", + description = "You have new messages.", canBeReplaced = true, isRedacted = false, isUpdated = false, @@ -633,7 +633,7 @@ class DefaultNotifiableEventResolverTest { roomId = A_ROOM_ID, eventId = AN_EVENT_ID, editedEventId = null, - description = "You have new message(s).", + description = "You have new messages.", canBeReplaced = true, isRedacted = false, isUpdated = false,