diff --git a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt index 9bed8eb48b9..f7a4937eac5 100644 --- a/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/CallNotificationManager.kt @@ -138,7 +138,7 @@ class CallNotificationManager @Inject constructor( hideIncomingCallNotification() } - fun hideIncomingCallNotification() { + private fun hideIncomingCallNotification() { appLogger.i("$TAG: hiding incoming call") // This delay is just so when the user receives two calling signals one straight after the other [INCOMING -> CANCEL] diff --git a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt index e66dc0e6459..9a0e23d37a4 100644 --- a/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt +++ b/app/src/main/kotlin/com/wire/android/notification/WireNotificationManager.kt @@ -18,7 +18,6 @@ package com.wire.android.notification -import android.os.Build import androidx.annotation.VisibleForTesting import com.wire.android.R import com.wire.android.appLogger @@ -270,7 +269,7 @@ class WireNotificationManager @Inject constructor( // start observing ongoing calls for all users, but only if not yet started if (observingJobs.ongoingCallJob.get().let { it == null || !it.isActive }) { val job = scope.launch(dispatcherProvider.default()) { - observeOngoingCalls(currentScreenState) + observeOngoingCalls() } observingJobs.ongoingCallJob.set(job) } @@ -405,34 +404,23 @@ class WireNotificationManager @Inject constructor( /** * Infinitely listen for the established calls of a current user and run OngoingCall foreground Service * to show corresponding notification and do not lose a call. - * @param currentScreenState StateFlow that informs which screen is currently visible, - * so we can listen established calls only when the app is in background. */ - private suspend fun observeOngoingCalls(currentScreenState: StateFlow) { - currentScreenState - .flatMapLatest { currentScreen -> - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && currentScreen !is CurrentScreen.InBackground) { - flowOf(null) + private suspend fun observeOngoingCalls() { + coreLogic.getGlobalScope().session.currentSessionFlow() + .flatMapLatest { + if (it is CurrentSessionResult.Success && it.accountInfo.isValid()) { + coreLogic.getSessionScope(it.accountInfo.userId).calls.establishedCall() + .map { it.isNotEmpty() } } else { - coreLogic.getGlobalScope().session.currentSessionFlow() - .flatMapLatest { - if (it is CurrentSessionResult.Success && it.accountInfo.isValid()) { - coreLogic.getSessionScope(it.accountInfo.userId).calls.establishedCall() - .map { - it.firstOrNull() - } - } else { - flowOf(null) - } - } + flowOf(false) } } .distinctUntilChanged() .onCompletion { servicesManager.stopOngoingCallService() } - .collect { call -> - if (call != null) servicesManager.startOngoingCallService() + .collect { isOngoingCall -> + if (isOngoingCall) servicesManager.startOngoingCallService() else servicesManager.stopOngoingCallService() } } @@ -492,12 +480,6 @@ class WireNotificationManager @Inject constructor( } } - data class MessagesNotificationsData( - val newNotifications: List, - val userId: QualifiedID, - val userName: String - ) - private data class UserObservingJobs( val currentScreenJob: Job, val incomingCallsJob: Job, diff --git a/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt b/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt index b9e39880786..6ef49a2f7d0 100644 --- a/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt +++ b/app/src/test/kotlin/com/wire/android/notification/WireNotificationManagerTest.kt @@ -77,6 +77,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch @@ -89,6 +90,7 @@ import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test import kotlin.time.Duration.Companion.minutes +@Suppress("LargeClass") @OptIn(ExperimentalCoroutinesApi::class) class WireNotificationManagerTest { @@ -144,24 +146,26 @@ class WireNotificationManagerTest { } @Test - fun givenSomeIncomingCalls_whenObserving_thenCallNotificationShowed() = runTestWithCancellation(dispatcherProvider.main()) { + fun givenSomeIncomingCall_whenObserving_thenCallHandleIncomingCallNotifications() = runTestWithCancellation(dispatcherProvider.main()) { + val userId = provideUserId("user1") + val incomingCalls = listOf(provideCall()) val (arrangement, manager) = Arrangement() - .withIncomingCalls(listOf()) - .withOutgoingCalls(listOf()) + .withSpecificUserSession(userId = userId, incomingCalls = incomingCalls) .withMessageNotifications(listOf()) .withCurrentScreen(CurrentScreen.SomeOther) - .withCurrentUserSession(CurrentSessionResult.Success(AccountInfo.Valid(provideUserId()))) + .withCurrentUserSession(CurrentSessionResult.Success(AccountInfo.Valid(userId))) .arrange() - manager.observeNotificationsAndCallsWhileRunning(listOf(provideUserId()), this) + manager.observeNotificationsAndCallsWhileRunning(listOf(userId), this) runCurrent() - verify(exactly = 0) { arrangement.callNotificationManager.hideIncomingCallNotification() } - verify(exactly = 1) { arrangement.callNotificationManager.handleIncomingCallNotifications(any(), any()) } + verify(exactly = 1) { + arrangement.callNotificationManager.handleIncomingCallNotifications(incomingCalls, userId) + } } @Test - fun givenSomeIncomingCall_whenCurrentUserIsDifferentFromCallReceiver_thenCallNotificationIsShown() = + fun givenSomeIncomingCall_whenCurrentUserIsDifferentFromCallReceiver_thenCallHandleIncomingCallNotifications() = runTestWithCancellation(dispatcherProvider.main()) { val user1 = provideUserId("user1") val user2 = provideUserId("user2") @@ -178,10 +182,7 @@ class WireNotificationManagerTest { runCurrent() verify(exactly = 1) { - arrangement.callNotificationManager.handleIncomingCallNotifications( - incomingCalls, - user2 - ) + arrangement.callNotificationManager.handleIncomingCallNotifications(incomingCalls, user2) } } @@ -544,7 +545,120 @@ class WireNotificationManagerTest { } @Test - fun givenAppInForeground_withValidCurrentAccountAndOngoingCall_whenObserving_thenStopOngoingCallService() = + fun givenAppInBackground_withTwoValidAccountsAndOngoingCallForNotCurrentOne_whenObserving_thenStopOngoingCallService() = + runTestWithCancellation(dispatcherProvider.main()) { + val userId1 = UserId("value1", "domain") + val userId2 = UserId("value2", "domain") + val call = provideCall().copy(status = CallStatus.ESTABLISHED) + val (arrangement, manager) = Arrangement() + .withCurrentScreen(CurrentScreen.InBackground) + .withSpecificUserSession(userId = userId1, establishedCalls = listOf()) + .withSpecificUserSession(userId = userId2, establishedCalls = listOf(call)) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId1.value))) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(userId1, userId2), this) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.stopOngoingCallService() } + } + + @Test + fun givenAppInBackground_withTwoValidAccountsAndOngoingCallForNotCurrentOne_whenCurrentAccountChanges_thenStartOngoingCallService() = + runTestWithCancellation(dispatcherProvider.main()) { + val userId1 = UserId("value1", "domain") + val userId2 = UserId("value2", "domain") + val call = provideCall().copy(status = CallStatus.ESTABLISHED) + val (arrangement, manager) = Arrangement() + .withCurrentScreen(CurrentScreen.InBackground) + .withSpecificUserSession(userId = userId1, establishedCalls = listOf()) + .withSpecificUserSession(userId = userId2, establishedCalls = listOf(call)) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId1.value))) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(userId1, userId2), this) + advanceUntilIdle() + + arrangement.withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId2.value))) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.startOngoingCallService() } + } + + @Test + fun givenAppInBackground_withTwoValidAccountsAndOngoingCallForCurrentOne_whenCurrentAccountChanges_thenStopOngoingCallService() = + runTestWithCancellation(dispatcherProvider.main()) { + val userId1 = UserId("value1", "domain") + val userId2 = UserId("value2", "domain") + val call = provideCall().copy(status = CallStatus.ESTABLISHED) + val (arrangement, manager) = Arrangement() + .withCurrentScreen(CurrentScreen.InBackground) + .withSpecificUserSession(userId = userId1, establishedCalls = listOf(call)) + .withSpecificUserSession(userId = userId2, establishedCalls = listOf()) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId1.value))) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(userId1, userId2), this) + advanceUntilIdle() + + arrangement.withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId2.value))) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.stopOngoingCallService() } + } + + @Test + fun givenAppInBackground_withValidCurrentAccountAndOngoingCall_whenAccountBecomesInvalid_thenStopOngoingCallService() = + runTestWithCancellation(dispatcherProvider.main()) { + val userId = provideUserId() + val call = provideCall().copy(status = CallStatus.ESTABLISHED) + val (arrangement, manager) = Arrangement() + .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) + .withMessageNotifications(listOf()) + .withCurrentScreen(CurrentScreen.InBackground) + .withEstablishedCall(listOf(call)) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId.value))) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(userId), this) + advanceUntilIdle() + + arrangement.withCurrentUserSession(CurrentSessionResult.Success(provideInvalidAccountInfo(userId.value))) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.stopOngoingCallService() } + } + + @Test + fun givenAppInBackground_whenValidCurrentAccountAndOngoingCall_whenThisCallChangesSomeState_thenDoNotStartOngoingCallServiceAgain() = + runTestWithCancellation(dispatcherProvider.main()) { + val userId = provideUserId() + val call = provideCall().copy(status = CallStatus.ESTABLISHED, isMuted = false) + val callFlow = MutableStateFlow(listOf(call)) + val (arrangement, manager) = Arrangement() + .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) + .withMessageNotifications(listOf()) + .withCurrentScreen(CurrentScreen.InBackground) + .withEstablishedCallFlow(callFlow) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId.value))) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(userId), this) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.startOngoingCallService() } + + val updatedCall = call.copy(isMuted = true) + callFlow.value = listOf(updatedCall) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.startOngoingCallService() } // started only once + } + + @Test + fun givenAppInForeground_withValidCurrentAccountAndOngoingCall_whenObserving_thenStartOngoingCallService() = runTestWithCancellation(dispatcherProvider.main()) { val userId = provideUserId() val call = provideCall().copy(status = CallStatus.ESTABLISHED) @@ -560,8 +674,8 @@ class WireNotificationManagerTest { manager.observeNotificationsAndCallsWhileRunning(listOf(userId), this) runCurrent() - verify(exactly = 0) { arrangement.servicesManager.startOngoingCallService() } - verify(exactly = 1) { arrangement.servicesManager.stopOngoingCallService() } + verify(exactly = 1) { arrangement.servicesManager.startOngoingCallService() } + verify(exactly = 0) { arrangement.servicesManager.stopOngoingCallService() } } @Test @@ -626,13 +740,13 @@ class WireNotificationManagerTest { } @Test - fun givenAppInBackground_withTwoValidAccountsAndOngoingCallForNotCurrentOne_whenObserving_thenStopOngoingCallService() = + fun givenAppInForeground_withTwoValidAccountsAndOngoingCallForNotCurrentOne_whenObserving_thenStopOngoingCallService() = runTestWithCancellation(dispatcherProvider.main()) { val userId1 = UserId("value1", "domain") val userId2 = UserId("value2", "domain") val call = provideCall().copy(status = CallStatus.ESTABLISHED) val (arrangement, manager) = Arrangement() - .withCurrentScreen(CurrentScreen.InBackground) + .withCurrentScreen(CurrentScreen.Home) .withSpecificUserSession(userId = userId1, establishedCalls = listOf()) .withSpecificUserSession(userId = userId2, establishedCalls = listOf(call)) .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId1.value))) @@ -645,13 +759,13 @@ class WireNotificationManagerTest { } @Test - fun givenAppInBackground_withTwoValidAccountsAndOngoingCallForNotCurrentOne_whenCurrentAccountChanges_thenStartOngoingCallService() = + fun givenAppInForeground_withTwoValidAccountsAndOngoingCallForNotCurrentOne_whenCurrentAccountChanges_thenStartOngoingCallService() = runTestWithCancellation(dispatcherProvider.main()) { val userId1 = UserId("value1", "domain") val userId2 = UserId("value2", "domain") val call = provideCall().copy(status = CallStatus.ESTABLISHED) val (arrangement, manager) = Arrangement() - .withCurrentScreen(CurrentScreen.InBackground) + .withCurrentScreen(CurrentScreen.Home) .withSpecificUserSession(userId = userId1, establishedCalls = listOf()) .withSpecificUserSession(userId = userId2, establishedCalls = listOf(call)) .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId1.value))) @@ -667,13 +781,13 @@ class WireNotificationManagerTest { } @Test - fun givenAppInBackground_withTwoValidAccountsAndOngoingCallForCurrentOne_whenCurrentAccountChanges_thenStopOngoingCallService() = + fun givenAppInForeground_withTwoValidAccountsAndOngoingCallForCurrentOne_whenCurrentAccountChanges_thenStopOngoingCallService() = runTestWithCancellation(dispatcherProvider.main()) { val userId1 = UserId("value1", "domain") val userId2 = UserId("value2", "domain") val call = provideCall().copy(status = CallStatus.ESTABLISHED) val (arrangement, manager) = Arrangement() - .withCurrentScreen(CurrentScreen.InBackground) + .withCurrentScreen(CurrentScreen.Home) .withSpecificUserSession(userId = userId1, establishedCalls = listOf(call)) .withSpecificUserSession(userId = userId2, establishedCalls = listOf()) .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId1.value))) @@ -689,7 +803,7 @@ class WireNotificationManagerTest { } @Test - fun givenAppInBackground_withValidCurrentAccountAndOngoingCall_whenAccountBecomesInvalid_thenStopOngoingCallService() = + fun givenAppInForeground_withValidCurrentAccountAndOngoingCall_whenAccountBecomesInvalid_thenStopOngoingCallService() = runTestWithCancellation(dispatcherProvider.main()) { val userId = provideUserId() val call = provideCall().copy(status = CallStatus.ESTABLISHED) @@ -697,7 +811,7 @@ class WireNotificationManagerTest { .withIncomingCalls(listOf()) .withOutgoingCalls(listOf()) .withMessageNotifications(listOf()) - .withCurrentScreen(CurrentScreen.InBackground) + .withCurrentScreen(CurrentScreen.Home) .withEstablishedCall(listOf(call)) .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId.value))) .arrange() @@ -711,6 +825,83 @@ class WireNotificationManagerTest { verify(exactly = 1) { arrangement.servicesManager.stopOngoingCallService() } } + @Test + fun givenAppInForeground_whenValidCurrentAccountAndOngoingCall_whenThisCallChangesSomeState_thenDoNotStartOngoingCallServiceAgain() = + runTestWithCancellation(dispatcherProvider.main()) { + val userId = provideUserId() + val call = provideCall().copy(status = CallStatus.ESTABLISHED, isMuted = false) + val callFlow = MutableStateFlow(listOf(call)) + val (arrangement, manager) = Arrangement() + .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) + .withMessageNotifications(listOf()) + .withCurrentScreen(CurrentScreen.Home) + .withEstablishedCallFlow(callFlow) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId.value))) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(userId), this) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.startOngoingCallService() } + + val updatedCall = call.copy(isMuted = true) + callFlow.value = listOf(updatedCall) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.startOngoingCallService() } // started only once + } + + @Test + fun givenAppInForegroundAndValidCurrentAccountAndOngoingCall_whenAppGoesIntoBackground_thenDoNotStartOngoingCallServiceAgain() = + runTestWithCancellation(dispatcherProvider.main()) { + val userId = provideUserId() + val call = provideCall().copy(status = CallStatus.ESTABLISHED) + val currentScreenFlow = MutableStateFlow(CurrentScreen.Home) + val (arrangement, manager) = Arrangement() + .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) + .withMessageNotifications(listOf()) + .withEstablishedCall(listOf(call)) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId.value))) + .withCurrentScreenFlow(currentScreenFlow) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(userId), this) + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.startOngoingCallService() } + + currentScreenFlow.value = CurrentScreen.InBackground + advanceUntilIdle() + + verify(exactly = 1) { arrangement.servicesManager.startOngoingCallService() } // started only once + } + + @Test + fun givenAppInBackgroundAndValidCurrentAccountAndOngoingCall_whenAppGoesIntoForeground_thenDoNotStopOngoingCallService() = + runTestWithCancellation(dispatcherProvider.main()) { + val userId = provideUserId() + val call = provideCall().copy(status = CallStatus.ESTABLISHED) + val currentScreenFlow = MutableStateFlow(CurrentScreen.InBackground) + val (arrangement, manager) = Arrangement() + .withIncomingCalls(listOf()) + .withOutgoingCalls(listOf()) + .withMessageNotifications(listOf()) + .withEstablishedCall(listOf(call)) + .withCurrentUserSession(CurrentSessionResult.Success(provideAccountInfo(userId.value))) + .withCurrentScreenFlow(currentScreenFlow) + .arrange() + + manager.observeNotificationsAndCallsWhileRunning(listOf(userId), this) + advanceUntilIdle() + + currentScreenFlow.value = CurrentScreen.Home + advanceUntilIdle() + + verify(exactly = 0) { arrangement.servicesManager.stopOngoingCallService() } + } + @Test fun givenSomeNotificationsAndUserBlockedByE2EIRequired_whenObserveCalled_thenNotificationIsNotShowed() = runTestWithCancellation(dispatcherProvider.main()) { @@ -898,7 +1089,6 @@ class WireNotificationManagerTest { coEvery { callsScope.establishedCall } returns establishedCall coEvery { callsScope.observeOutgoingCall } returns observeOutgoingCall coEvery { callNotificationManager.handleIncomingCallNotifications(any(), any()) } returns Unit - coEvery { callNotificationManager.hideIncomingCallNotification() } returns Unit coEvery { callNotificationManager.builder.getNotificationTitle(any()) } returns "Test title" coEvery { messageScope.getNotifications } returns getNotificationsUseCase coEvery { messageScope.markMessagesAsNotified } returns markMessagesAsNotified @@ -971,6 +1161,10 @@ class WireNotificationManagerTest { return this } + fun withEstablishedCallFlow(callsFlow: Flow>): Arrangement = apply { + coEvery { establishedCall() } returns callsFlow + } + @Suppress("LongParameterList") fun withSpecificUserSession( userId: UserId, @@ -988,6 +1182,10 @@ class WireNotificationManagerTest { return this } + fun withCurrentScreenFlow(screenFlow: StateFlow): Arrangement = apply { + coEvery { currentScreenManager.observeCurrentScreen(any()) } returns screenFlow + } + fun withSelfUser(selfUserFlow: Flow) = apply { coEvery { getSelfUser.invoke() } returns selfUserFlow }