diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt index 803c9d8f42..6e57100781 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/notification/NotificationData.kt @@ -30,6 +30,7 @@ fun aNotificationData( senderIsNameAmbiguous: Boolean = false, roomDisplayName: String? = A_ROOM_NAME, roomJoinRule: JoinRule? = null, + isNoisy: Boolean = false, ): NotificationData { return NotificationData( sessionId = A_SESSION_ID, @@ -45,7 +46,7 @@ fun aNotificationData( isDm = false, isSpace = isSpace, isEncrypted = false, - isNoisy = false, + isNoisy = isNoisy, timestamp = timestamp, content = content, hasMention = hasMention, 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 fdffad3049..e22ae1f7d1 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 @@ -298,6 +298,7 @@ class DefaultNotifiableEventResolver( roomId = roomId, eventId = eventId, cause = "Unable to decrypt event content", + noisy = isNoisy, ) ResolvedPushEvent.Event(fallbackNotifiableEvent) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index b6d15dac4e..6834f404ed 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -274,6 +274,12 @@ private fun NotifiableEvent.toFallbackNotifiableEvent(): FallbackNotifiableEvent is FallbackNotifiableEvent -> timestamp is NotifiableRingingCallEvent -> return null } + val noisy = when (this) { + is NotifiableMessageEvent -> noisy + is InviteNotifiableEvent -> noisy + is SimpleNotifiableEvent -> noisy + is FallbackNotifiableEvent -> noisy + } return FallbackNotifiableEvent( sessionId = sessionId, roomId = roomId, @@ -283,6 +289,7 @@ private fun NotifiableEvent.toFallbackNotifiableEvent(): FallbackNotifiableEvent canBeReplaced = false, isRedacted = false, isUpdated = false, + noisy = noisy, timestamp = timestamp, cause = null, ) 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 index 970f19fd45..d2083f7333 100644 --- 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 @@ -24,6 +24,7 @@ class FallbackNotificationFactory( roomId: RoomId, eventId: EventId, cause: String?, + noisy: Boolean, ): FallbackNotifiableEvent = FallbackNotifiableEvent( sessionId = sessionId, roomId = roomId, @@ -32,6 +33,7 @@ class FallbackNotificationFactory( canBeReplaced = true, isRedacted = false, isUpdated = false, + noisy = noisy, timestamp = clock.epochMillis(), description = "", cause = cause, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt index a97937f5d0..88ba6c5ff9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResultProcessor.kt @@ -170,6 +170,7 @@ class DefaultNotificationResultProcessor( roomId = RoomId(request.roomId), eventId = EventId(request.eventId), cause = exception.message, + noisy = false, ) ) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt index afbbe4af4f..26412abebc 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationCreator.kt @@ -317,7 +317,7 @@ class DefaultNotificationCreator( val fallbackNotifiableEvent = fallbackNotifiableEvents.first() val channelId = notificationChannels.getChannelIdForMessage( sessionId = fallbackNotifiableEvent.sessionId, - noisy = false, + noisy = fallbackNotifiableEvents.any { it.noisy }, ) val existingCounter = existingNotification ?.extras diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/FallbackNotifiableEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/FallbackNotifiableEvent.kt index 97f66face0..3b3210a580 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/FallbackNotifiableEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/FallbackNotifiableEvent.kt @@ -25,6 +25,7 @@ data class FallbackNotifiableEvent( override val canBeReplaced: Boolean, override val isRedacted: Boolean, override val isUpdated: Boolean, + val noisy: Boolean, val timestamp: Long, val cause: String?, ) : NotifiableEvent 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 24e88e72d2..5918329c40 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 @@ -712,6 +712,41 @@ class DefaultNotifiableEventResolverTest : RobolectricTest() { canBeReplaced = true, isRedacted = false, isUpdated = false, + noisy = false, + timestamp = A_FAKE_TIMESTAMP, + cause = "Unable to decrypt event content", + ) + ) + assertThat(result.getEvent(request)).isEqualTo(Result.success(expectedResult)) + } + + @Test + fun `resolve RoomEncrypted noisy`() = runTest { + val sut = createDefaultNotifiableEventResolver( + notificationResult = Result.success( + mapOf( + AN_EVENT_ID to Result.success( + aNotificationData( + content = NotificationContent.MessageLike.RoomEncrypted, + isNoisy = true, + ) + ) + ) + ) + ) + val request = aPushRequest(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 = "", + canBeReplaced = true, + isRedacted = false, + isUpdated = false, + noisy = true, timestamp = A_FAKE_TIMESTAMP, cause = "Unable to decrypt event content", ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt index 4e2685e63a..cdc6a01fad 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -15,28 +15,35 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.test.FakeLockScreenService +import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 import io.element.android.libraries.matrix.test.A_THREAD_ID import io.element.android.libraries.matrix.test.A_THREAD_ID_2 +import io.element.android.libraries.matrix.test.A_TIMESTAMP import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.media.test.FakeImageLoaderHolder import io.element.android.libraries.push.api.notifications.NotificationIdProvider +import io.element.android.libraries.push.impl.notifications.factories.NotificationAccountParams +import io.element.android.libraries.push.impl.notifications.factories.NotificationCreator import io.element.android.libraries.push.impl.notifications.factories.aNotificationAccountParams import io.element.android.libraries.push.impl.notifications.fake.FakeActiveNotificationsProvider import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationCreator import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationDisplayer import io.element.android.libraries.push.impl.notifications.fake.FakeRoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGroupMessageCreator +import io.element.android.libraries.push.impl.notifications.fixtures.A_NOTIFICATION import io.element.android.libraries.push.impl.notifications.fixtures.aFallbackNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent +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.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.observer.SessionObserver @@ -48,6 +55,8 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.test.FakeAppNavigationStateService import io.element.android.services.appnavstate.test.aNavigationState import io.element.android.services.appnavstate.test.anAppNavigationState +import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP +import io.element.android.tests.testutils.lambda.LambdaThreeParamsRecorder import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -472,6 +481,137 @@ class DefaultNotificationDrawerManagerTest { extraInvocationsForNotificationSummary = 1, ) + @Test + fun `when the app is locked, a noisy message event produces a noisy fallback notification`() = testAppLockedNotifiableEventReceived( + notifiableEvent = aNotifiableMessageEvent(noisy = true), + expectedFallbackEvent = anExpectedFallbackNotifiableEvent(noisy = true, timestamp = A_TIMESTAMP), + ) + + @Test + fun `when the app is locked, a silent message event produces a silent fallback notification`() = testAppLockedNotifiableEventReceived( + notifiableEvent = aNotifiableMessageEvent(noisy = false), + expectedFallbackEvent = anExpectedFallbackNotifiableEvent(noisy = false, timestamp = A_TIMESTAMP), + ) + + @Test + fun `when the app is locked, a noisy invite event produces a noisy fallback notification`() = testAppLockedNotifiableEventReceived( + notifiableEvent = anInviteNotifiableEvent(noisy = true), + expectedFallbackEvent = anExpectedFallbackNotifiableEvent(noisy = true, timestamp = 0), + ) + + @Test + fun `when the app is locked, a silent invite event produces a silent fallback notification`() = testAppLockedNotifiableEventReceived( + notifiableEvent = anInviteNotifiableEvent(noisy = false), + expectedFallbackEvent = anExpectedFallbackNotifiableEvent(noisy = false, timestamp = 0), + ) + + @Test + fun `when the app is locked, a noisy simple event produces a noisy fallback notification`() = testAppLockedNotifiableEventReceived( + notifiableEvent = aSimpleNotifiableEvent(noisy = true), + expectedFallbackEvent = anExpectedFallbackNotifiableEvent(noisy = true, timestamp = 0), + ) + + @Test + fun `when the app is locked, a silent simple event produces a silent fallback notification`() = testAppLockedNotifiableEventReceived( + notifiableEvent = aSimpleNotifiableEvent(noisy = false), + expectedFallbackEvent = anExpectedFallbackNotifiableEvent(noisy = false, timestamp = 0), + ) + + @Test + fun `when the app is locked, a noisy fallback event stays noisy`() = testAppLockedNotifiableEventReceived( + notifiableEvent = aFallbackNotifiableEvent(noisy = true), + expectedFallbackEvent = anExpectedFallbackNotifiableEvent(noisy = true, timestamp = A_FAKE_TIMESTAMP), + ) + + @Test + fun `when the app is locked, a silent fallback event stays silent`() = testAppLockedNotifiableEventReceived( + notifiableEvent = aFallbackNotifiableEvent(noisy = false), + expectedFallbackEvent = anExpectedFallbackNotifiableEvent(noisy = false, timestamp = A_FAKE_TIMESTAMP), + ) + + @Test + fun `when the app is locked, all the events are grouped into a single fallback notification`() = runTest { + val createFallbackNotificationResult = aCreateFallbackNotificationRecorder() + val sut = createAppLockedNotificationDrawerManager(createFallbackNotificationResult) + sut.onNotifiableEventsReceived( + listOf( + aNotifiableMessageEvent(noisy = false), + aNotifiableMessageEvent(eventId = AN_EVENT_ID_2, noisy = true), + ) + ) + createFallbackNotificationResult.assertions() + .isCalledOnce() + .with( + value(null), + any(), + value( + listOf( + anExpectedFallbackNotifiableEvent(noisy = false, timestamp = A_TIMESTAMP), + anExpectedFallbackNotifiableEvent(eventId = AN_EVENT_ID_2, noisy = true, timestamp = A_TIMESTAMP), + ) + ), + ) + } + + @Test + fun `when the app is not locked, no fallback notification is created`() = runTest { + val createFallbackNotificationResult = aCreateFallbackNotificationRecorder() + val sut = createDefaultNotificationDrawerManager( + appNavigationStateService = FakeAppNavigationStateService( + initialAppNavigationState = anAppNavigationState(isInForeground = false), + ), + notificationCreator = FakeNotificationCreator( + createFallbackNotificationResult = createFallbackNotificationResult, + ), + lockScreenService = FakeLockScreenService().apply { setIsPinSetup(false) }, + ) + sut.onNotifiableEventsReceived(listOf(aNotifiableMessageEvent(noisy = true))) + createFallbackNotificationResult.assertions().isNeverCalled() + } + + /** + * The content of the original event must be stripped, only the noisy flag, the ids and the timestamp are kept. + */ + private fun anExpectedFallbackNotifiableEvent( + eventId: EventId = AN_EVENT_ID, + noisy: Boolean, + timestamp: Long, + ) = aFallbackNotifiableEvent( + eventId = eventId, + description = null, + canBeReplaced = false, + noisy = noisy, + timestamp = timestamp, + cause = null, + ) + + private fun aCreateFallbackNotificationRecorder() = + lambdaRecorder, Notification> { _, _, _ -> A_NOTIFICATION } + + private fun TestScope.createAppLockedNotificationDrawerManager( + createFallbackNotificationResult: LambdaThreeParamsRecorder, Notification>, + ) = createDefaultNotificationDrawerManager( + appNavigationStateService = FakeAppNavigationStateService( + initialAppNavigationState = anAppNavigationState(isInForeground = false), + ), + notificationCreator = FakeNotificationCreator( + createFallbackNotificationResult = createFallbackNotificationResult, + ), + lockScreenService = FakeLockScreenService().apply { setIsPinSetup(true) }, + ) + + private fun testAppLockedNotifiableEventReceived( + notifiableEvent: NotifiableEvent, + expectedFallbackEvent: FallbackNotifiableEvent, + ) = runTest { + val createFallbackNotificationResult = aCreateFallbackNotificationRecorder() + val sut = createAppLockedNotificationDrawerManager(createFallbackNotificationResult) + sut.onNotifiableEventsReceived(listOf(notifiableEvent)) + createFallbackNotificationResult.assertions() + .isCalledOnce() + .with(value(null), any(), value(listOf(expectedFallbackEvent))) + } + private fun testOnNotifiableEventReceived( appNavigationState: AppNavigationState, notifiableEvents: List, @@ -503,6 +643,7 @@ class DefaultNotificationDrawerManagerTest { fun TestScope.createDefaultNotificationDrawerManager( notificationDisplayer: NotificationDisplayer = FakeNotificationDisplayer(), notificationRenderer: NotificationRenderer? = null, + notificationCreator: NotificationCreator = FakeNotificationCreator(), appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(), roomGroupMessageCreator: RoomGroupMessageCreator = FakeRoomGroupMessageCreator(), summaryGroupMessageCreator: SummaryGroupMessageCreator = FakeSummaryGroupMessageCreator(), @@ -519,7 +660,7 @@ fun TestScope.createDefaultNotificationDrawerManager( notificationRenderer = notificationRenderer ?: NotificationRenderer( notificationDisplayer = notificationDisplayer, notificationDataFactory = DefaultNotificationDataFactory( - notificationCreator = FakeNotificationCreator(), + notificationCreator = notificationCreator, roomGroupMessageCreator = roomGroupMessageCreator, summaryGroupMessageCreator = summaryGroupMessageCreator, activeNotificationsProvider = activeNotificationsProvider, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt index 91f29dd28e..21a2c6cb91 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationResultProcessorTest.kt @@ -267,6 +267,42 @@ class DefaultNotificationResultProcessorTest { assertThat(receivedFallbackEvent).isTrue() } + @Test + fun `when an event cannot be resolved, the generated fallback event is silent`() = runTest { + val onNotifiableEventsReceived = lambdaRecorder, Unit> {} + val onPushReceivedResult = lambdaRecorder { _, _, _, _, _, _, _ -> } + val processor = createDefaultNotificationResultProcessor( + mutableBatteryOptimizationStore = FakeMutableBatteryOptimizationStore( + showBatteryOptimizationBannerResult = lambdaRecorder {}, + ), + pushHistoryService = FakePushHistoryService( + onPushReceivedResult = onPushReceivedResult, + ), + onNotifiableEventsReceived = onNotifiableEventsReceived, + ) + + runningProcessor(processor) { + emit(mapOf(aPushRequest() to Result.failure(NotificationResolverException.UnknownError("Unable to resolve event")))) + } + + advanceTimeBy(300.milliseconds) + + onNotifiableEventsReceived.assertions() + .isCalledOnce() + .with( + value( + listOf( + aFallbackNotifiableEvent( + description = "", + canBeReplaced = true, + noisy = false, + cause = "Unable to resolve event", + ) + ) + ) + ) + } + private suspend fun TestScope.runningProcessor(processor: NotificationResultProcessor, block: suspend NotificationResultProcessor.() -> Unit) { processor.start() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactoryTest.kt new file mode 100644 index 0000000000..58825f07b9 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/FallbackNotificationFactoryTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2026 Element Creations 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 com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +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.push.impl.notifications.fixtures.aFallbackNotifiableEvent +import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import org.junit.Test + +class FallbackNotificationFactoryTest { + @Test + fun `create silent event`() { + val sut = FallbackNotificationFactory(FakeSystemClock()) + val result = sut.create( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + cause = "A cause", + noisy = false, + ) + assertThat(result).isEqualTo( + aFallbackNotifiableEvent( + description = "", + canBeReplaced = true, + noisy = false, + cause = "A cause", + ) + ) + } + + @Test + fun `create noisy event`() { + val sut = FallbackNotificationFactory(FakeSystemClock()) + val result = sut.create( + sessionId = A_SESSION_ID, + roomId = A_ROOM_ID, + eventId = AN_EVENT_ID, + cause = "A cause", + noisy = true, + ) + assertThat(result).isEqualTo( + aFallbackNotifiableEvent( + description = "", + canBeReplaced = true, + noisy = true, + cause = "A cause", + ) + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt index 299c5ad213..ead16c02d7 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/factories/DefaultNotificationCreatorTest.kt @@ -18,6 +18,7 @@ import io.element.android.appconfig.NotificationConfig import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.libraries.core.meta.BuildMeta +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_EVENT_ID_2 import io.element.android.libraries.matrix.test.A_COLOR_INT @@ -33,6 +34,7 @@ import io.element.android.libraries.push.impl.notifications.DefaultNotificationB import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.libraries.push.impl.notifications.channels.DefaultNotificationChannels +import io.element.android.libraries.push.impl.notifications.channels.FakeNotificationChannels import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory @@ -47,6 +49,8 @@ import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvid 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 io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.robolectric.RobolectricTest import kotlinx.coroutines.test.runTest import org.junit.Test @@ -82,7 +86,10 @@ class DefaultNotificationCreatorTest : RobolectricTest() { @Test fun `test createFallbackNotification`() { - val sut = createNotificationCreator() + val channelIdForMessage = lambdaRecorder { _, _ -> A_CHANNEL_ID } + val sut = createNotificationCreator( + notificationChannels = FakeNotificationChannels(channelIdForMessage = channelIdForMessage), + ) val result = sut.createFallbackNotification( existingNotification = null, notificationAccountParams = aNotificationAccountParams(), @@ -93,6 +100,50 @@ class DefaultNotificationCreatorTest : RobolectricTest() { result.commonAssertions( expectedCategory = null, ) + assertThat(result.channelId).isEqualTo(A_CHANNEL_ID) + // The notification must be posted on the silent channel. + channelIdForMessage.assertions().isCalledOnce().with(value(A_SESSION_ID), value(false)) + } + + @Test + fun `test createFallbackNotification noisy`() { + val channelIdForMessage = lambdaRecorder { _, _ -> A_CHANNEL_ID } + val sut = createNotificationCreator( + notificationChannels = FakeNotificationChannels(channelIdForMessage = channelIdForMessage), + ) + val result = sut.createFallbackNotification( + existingNotification = null, + notificationAccountParams = aNotificationAccountParams(), + fallbackNotifiableEvents = listOf( + aFallbackNotifiableEvent(noisy = true), + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + assertThat(result.channelId).isEqualTo(A_CHANNEL_ID) + // The notification must be posted on the noisy channel. + channelIdForMessage.assertions().isCalledOnce().with(value(A_SESSION_ID), value(true)) + } + + @Test + fun `test createFallbackNotification is noisy when at least one event is noisy`() { + val channelIdForMessage = lambdaRecorder { _, _ -> A_CHANNEL_ID } + val sut = createNotificationCreator( + notificationChannels = FakeNotificationChannels(channelIdForMessage = channelIdForMessage), + ) + val result = sut.createFallbackNotification( + existingNotification = null, + notificationAccountParams = aNotificationAccountParams(), + fallbackNotifiableEvents = listOf( + aFallbackNotifiableEvent(noisy = false), + aFallbackNotifiableEvent(eventId = AN_EVENT_ID_2, noisy = true), + ) + ) + result.commonAssertions( + expectedCategory = null, + ) + channelIdForMessage.assertions().isCalledOnce().with(value(A_SESSION_ID), value(true)) } @Test @@ -389,6 +440,8 @@ class DefaultNotificationCreatorTest : RobolectricTest() { } } +private const val A_CHANNEL_ID = "aChannelId" + const val MARK_AS_READ_ACTION_TITLE = "MarkAsReadAction" const val QUICK_REPLY_ACTION_TITLE = "QuickReplyAction" const val ACCEPT_INVITATION_ACTION_TITLE = "AcceptInvitationAction" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt index 4242632fe5..cb4bffcfb3 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fixtures/NotifiableEventFixture.kt @@ -39,13 +39,14 @@ fun aSimpleNotifiableEvent( type: String? = null, isRedacted: Boolean = false, canBeReplaced: Boolean = false, - editedEventId: EventId? = null + editedEventId: EventId? = null, + noisy: Boolean = false, ) = SimpleNotifiableEvent( sessionId = sessionId, roomId = roomId, eventId = eventId, editedEventId = editedEventId, - noisy = false, + noisy = noisy, title = "title", description = "description", type = type, @@ -59,14 +60,15 @@ fun anInviteNotifiableEvent( sessionId: SessionId = A_SESSION_ID, roomId: RoomId = A_ROOM_ID, eventId: EventId = AN_EVENT_ID, - isRedacted: Boolean = false + isRedacted: Boolean = false, + noisy: Boolean = false, ) = InviteNotifiableEvent( sessionId = sessionId, eventId = eventId, roomId = roomId, roomName = A_ROOM_NAME, editedEventId = null, - noisy = false, + noisy = noisy, title = "title", description = "description", type = null, @@ -89,11 +91,12 @@ fun aNotifiableMessageEvent( senderId: UserId = A_USER_ID_2, senderDisambiguatedDisplayName: String = A_USER_NAME_2, roomName: String? = A_ROOM_NAME, + noisy: Boolean = false, ) = NotifiableMessageEvent( sessionId = sessionId, eventId = eventId, editedEventId = null, - noisy = false, + noisy = noisy, timestamp = timestamp, senderDisambiguatedDisplayName = senderDisambiguatedDisplayName, senderId = senderId, @@ -149,15 +152,23 @@ fun aNotifiableCallEvent( fun aFallbackNotifiableEvent( sessionId: SessionId = A_SESSION_ID, + roomId: RoomId = A_ROOM_ID, + eventId: EventId = AN_EVENT_ID, + description: String? = "A fallback notification", + canBeReplaced: Boolean = false, + noisy: Boolean = false, + timestamp: Long = A_FAKE_TIMESTAMP, + cause: String? = "Unable to decrypt event", ) = FallbackNotifiableEvent( sessionId = sessionId, - roomId = A_ROOM_ID, - eventId = AN_EVENT_ID, + roomId = roomId, + eventId = eventId, editedEventId = null, - description = "A fallback notification", - canBeReplaced = false, + description = description, + canBeReplaced = canBeReplaced, isRedacted = false, isUpdated = false, - timestamp = A_FAKE_TIMESTAMP, - cause = "Unable to decrypt event", + noisy = noisy, + timestamp = timestamp, + cause = cause, )