diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt index d774a5133c..8bc77caaa3 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt @@ -8,6 +8,7 @@ package io.element.android.features.call.impl.utils import android.annotation.SuppressLint +import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationManagerCompat import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.ElementCallConfig @@ -56,11 +57,6 @@ interface ActiveCallManager { */ fun registerIncomingCall(notificationData: CallNotificationData) - /** - * Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification. - */ - fun incomingCallTimedOut() - /** * Called when the active call has been hung up. It will remove any existing UI and the active call. * @param callType The type of call that the user hung up, either an external url one or a room one. @@ -113,18 +109,24 @@ class DefaultActiveCallManager @Inject constructor( // Wait for the ringing call to time out delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds) - incomingCallTimedOut() + incomingCallTimedOut(displayMissedCallNotification = true) } } - override fun incomingCallTimedOut() { + /** + * Called when the incoming call timed out. It will remove the active call and remove any associated UI, adding a 'missed call' notification. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun incomingCallTimedOut(displayMissedCallNotification: Boolean) { val previousActiveCall = activeCall.value ?: return val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return activeCall.value = null cancelIncomingCallNotification() - displayMissedCallNotification(notificationData) + if (displayMissedCallNotification) { + displayMissedCallNotification(notificationData) + } } override fun hungUpCall(callType: CallType) { @@ -186,28 +188,35 @@ class DefaultActiveCallManager @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun observeRingingCall() { - // This will observe ringing calls and ensure they're terminated if the room call is cancelled + // This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user + // has joined the call from another session. activeCall .filterNotNull() .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall } .flatMapLatest { activeCall -> val callType = activeCall.callType as CallType.RoomCall - // Get a flow of updated `hasRoomCall` values for the room + // Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?.getRoom(callType.roomId) ?.roomInfoFlow - ?.map { it.hasRoomCall } + ?.map { + it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants) + } ?: flowOf() } // We only want to check if the room active call status changes .distinctUntilChanged() // Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway) .drop(1) - .onEach { roomHasActiveCall -> + .onEach { (roomHasActiveCall, userIsInTheCall) -> if (!roomHasActiveCall) { // The call was cancelled timedOutCallJob?.cancel() - incomingCallTimedOut() + incomingCallTimedOut(displayMissedCallNotification = true) + } else if (userIsInTheCall) { + // The user joined the call from another session + timedOutCallJob?.cancel() + incomingCallTimedOut(displayMissedCallNotification = false) } } .launchIn(coroutineScope) diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt index b9f6a524ae..fe5454fb67 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt @@ -119,7 +119,7 @@ class DefaultActiveCallManagerTest { onMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(addMissedCallNotificationLambda = addMissedCallNotificationLambda) ) - manager.incomingCallTimedOut() + manager.incomingCallTimedOut(displayMissedCallNotification = true) addMissedCallNotificationLambda.assertions().isNeverCalled() } @@ -139,7 +139,7 @@ class DefaultActiveCallManagerTest { manager.registerIncomingCall(aCallNotificationData()) assertThat(manager.activeCall.value).isNotNull() - manager.incomingCallTimedOut() + manager.incomingCallTimedOut(displayMissedCallNotification = true) advanceTimeBy(1) assertThat(manager.activeCall.value).isNull() diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt index 8ec385d861..9554a46dae 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.MutableStateFlow class FakeActiveCallManager( var registerIncomingCallResult: (CallNotificationData) -> Unit = {}, - var incomingCallTimedOutResult: () -> Unit = {}, var hungUpCallResult: (CallType) -> Unit = {}, var joinedCallResult: (CallType) -> Unit = {}, ) : ActiveCallManager { @@ -25,10 +24,6 @@ class FakeActiveCallManager( registerIncomingCallResult(notificationData) } - override fun incomingCallTimedOut() { - incomingCallTimedOutResult() - } - override fun hungUpCall(callType: CallType) { hungUpCallResult(callType) }