diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 03ae56acd5..f72a0886a0 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { testImplementation(libs.coil.test) testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.toolbox.impl) testImplementation(projects.services.toolbox.test) 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 f2e3240203..09f622e978 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 @@ -36,6 +36,7 @@ import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.currentSessionId import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -61,6 +62,8 @@ class DefaultNotificationDrawerManager @Inject constructor( private val buildMeta: BuildMeta, private val matrixClientProvider: MatrixClientProvider, ) : NotificationDrawerManager { + private var appNavigationStateObserver: Job? = null + /** * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. */ @@ -72,12 +75,17 @@ class DefaultNotificationDrawerManager @Inject constructor( init { // Observe application state - coroutineScope.launch { + appNavigationStateObserver = coroutineScope.launch { appNavigationStateService.appNavigationState .collect { onAppNavigationStateChange(it.navigationState) } } } + // For test only + fun destroy() { + appNavigationStateObserver?.cancel() + } + private var currentAppNavigationState: NavigationState? = null private fun onAppNavigationStateChange(navigationState: NavigationState) { 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 new file mode 100644 index 0000000000..6b4ea24fd4 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManagerTest.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +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.matrix.test.A_SPACE_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID +import io.element.android.libraries.matrix.test.FakeMatrixClientProvider +import io.element.android.libraries.matrix.test.core.aBuildMeta +import io.element.android.libraries.push.impl.notifications.fake.FakeAndroidNotificationFactory +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.aNotifiableMessageEvent +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent +import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.NavigationState +import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import io.element.android.services.appnavstate.test.aNavigationState +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class DefaultNotificationDrawerManagerTest { + @Test + fun `clearAllEvents should have no effect when queue is empty`() = runTest { + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() + defaultNotificationDrawerManager.clearAllEvents(A_SESSION_ID) + defaultNotificationDrawerManager.destroy() + } + + @Test + fun `cover all APIs`() = runTest { + // For now just call all the API. Later, add more valuable tests. + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager() + defaultNotificationDrawerManager.notificationStyleChanged() + defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = true) + defaultNotificationDrawerManager.clearAllMessagesEvents(A_SESSION_ID, doRender = false) + defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = true) + defaultNotificationDrawerManager.clearEvent(A_SESSION_ID, AN_EVENT_ID, doRender = false) + defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true) + defaultNotificationDrawerManager.clearMessagesForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false) + defaultNotificationDrawerManager.clearMembershipNotificationForSession(A_SESSION_ID) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = true) + defaultNotificationDrawerManager.clearMembershipNotificationForRoom(A_SESSION_ID, A_ROOM_ID, doRender = false) + defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) + // Add the same Event again (will be ignored) + defaultNotificationDrawerManager.onNotifiableEventReceived(aNotifiableMessageEvent()) + defaultNotificationDrawerManager.destroy() + } + + @Test + fun `react to applicationStateChange`() = runTest { + // For now just call all the API. Later, add more valuable tests. + val appNavigationStateFlow: MutableStateFlow = MutableStateFlow( + AppNavigationState( + navigationState = NavigationState.Root, + isInForeground = true, + ) + ) + val appNavigationStateService = FakeAppNavigationStateService(appNavigationState = appNavigationStateFlow) + val defaultNotificationDrawerManager = createDefaultNotificationDrawerManager( + appNavigationStateService = appNavigationStateService + ) + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID), isInForeground = true)) + runCurrent() + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID), isInForeground = true)) + runCurrent() + // Like a user sign out + appNavigationStateFlow.emit(AppNavigationState(aNavigationState(), isInForeground = true)) + runCurrent() + defaultNotificationDrawerManager.destroy() + } + + private fun TestScope.createDefaultNotificationDrawerManager( + appNavigationStateService: AppNavigationStateService = FakeAppNavigationStateService(), + initialData: List = emptyList() + ): DefaultNotificationDrawerManager { + val context = RuntimeEnvironment.getApplication() + return DefaultNotificationDrawerManager( + notifiableEventProcessor = NotifiableEventProcessor( + outdatedDetector = OutdatedEventDetector(), + appNavigationStateService = appNavigationStateService + ), + notificationRenderer = NotificationRenderer( + NotificationIdProvider(), + NotificationDisplayer(context), + NotificationFactory( + FakeAndroidNotificationFactory().instance, + FakeRoomGroupMessageCreator().instance, + FakeSummaryGroupMessageCreator().instance, + ) + ), + notificationEventPersistence = InMemoryNotificationEventPersistence(initialData = initialData), + filteredEventDetector = FilteredEventDetector(), + appNavigationStateService = appNavigationStateService, + coroutineScope = this, + dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), + buildMeta = aBuildMeta(), + matrixClientProvider = FakeMatrixClientProvider(), + ) + } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt new file mode 100644 index 0000000000..09f907f50b --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/InMemoryNotificationEventPersistence.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.notifications + +import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent + +class InMemoryNotificationEventPersistence( + initialData: List = emptyList() +) : NotificationEventPersistence { + private var data: List = initialData + + override fun loadEvents(factory: (List) -> NotificationEventQueue): NotificationEventQueue { + return factory(data) + } + + override fun persistEvents(queuedEvents: NotificationEventQueue) { + data = queuedEvents.rawEvents() + } +} diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt index a09e2a9c5e..ac6060fa8b 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt @@ -20,23 +20,19 @@ 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.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.services.appnavstate.api.NavigationState -import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.NavigationState import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow class FakeAppNavigationStateService( - private val fakeAppNavigationState: MutableStateFlow = MutableStateFlow( + override val appNavigationState: MutableStateFlow = MutableStateFlow( AppNavigationState( navigationState = NavigationState.Root, isInForeground = true, ) ), ) : AppNavigationStateService { - - override val appNavigationState: StateFlow = fakeAppNavigationState - override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit override fun onLeavingSession(owner: String) = Unit