Revert "Make sure declining a call stops observing the ringing call state (#5…" (#5615)

This reverts commit 10bf5f1c8c.
This commit is contained in:
Jorge Martin Espinosa 2025-10-27 17:25:56 +01:00 committed by Jorge Martín
parent df004db427
commit 444ae96030
2 changed files with 64 additions and 117 deletions

View file

@ -28,9 +28,7 @@ import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.room.BaseRoom
import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder import io.element.android.libraries.matrix.ui.media.ImageLoaderHolder
import io.element.android.libraries.push.api.notifications.ForegroundServiceType import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider import io.element.android.libraries.push.api.notifications.NotificationIdProvider
@ -41,17 +39,16 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@ -183,7 +180,13 @@ class DefaultActiveCallManager(
val previousActiveCall = activeCall.value ?: return val previousActiveCall = activeCall.value ?: return
val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return
removeCurrentCall() activeCall.value = null
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after timeout")
activeWakeLock.release()
}
cancelIncomingCallNotification()
if (displayMissedCallNotification) { if (displayMissedCallNotification) {
displayMissedCallNotification(notificationData) displayMissedCallNotification(notificationData)
@ -208,16 +211,24 @@ class DefaultActiveCallManager(
?.declineCall(notificationData.eventId) ?.declineCall(notificationData.eventId)
} }
removeCurrentCall() cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after hang up")
activeWakeLock.release()
}
timedOutCallJob?.cancel()
activeCall.value = null
} }
/**
* Removes the current active call and any associated UI, cancelling the timeouts and wakelocks.
*/
override suspend fun joinedCall(callType: CallType) = mutex.withLock { override suspend fun joinedCall(callType: CallType) = mutex.withLock {
Timber.tag(tag).d("Joined call: $callType") Timber.tag(tag).d("Joined call: $callType")
removeCurrentCall() cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after joining call")
activeWakeLock.release()
}
timedOutCallJob?.cancel()
activeCall.value = ActiveCall( activeCall.value = ActiveCall(
callType = callType, callType = callType,
@ -225,23 +236,6 @@ class DefaultActiveCallManager(
) )
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun removeCurrentCall() {
// Cancel and remove the timeout call job, if any
timedOutCallJob?.cancel()
timedOutCallJob = null
// Remove the active call and cancel the notification
activeCall.value = null
cancelIncomingCallNotification()
// Also remove any wake locks that may be held
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after call declined from another session")
activeWakeLock.release()
}
}
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) { private suspend fun showIncomingCallNotification(notificationData: CallNotificationData) {
Timber.tag(tag).d("Displaying ringing call notification") Timber.tag(tag).d("Displaying ringing call notification")
@ -287,75 +281,73 @@ class DefaultActiveCallManager(
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun observeRingingCall() { private fun observeRingingCall() {
val roomForActiveCallFlow: Flow<Pair<BaseRoom, EventId>?> = activeCall.mapLatest { activeCall -> activeCall
val callType = activeCall?.callType as? CallType.RoomCall ?: return@mapLatest null .filterNotNull()
val ringingInfo = activeCall.callState as? CallState.Ringing ?: return@mapLatest null .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run { .flatMapLatest { activeCall ->
Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall") val callType = activeCall.callType as CallType.RoomCall
return@mapLatest null val ringingInfo = activeCall.callState as CallState.Ringing
} val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run {
val room = client.getRoom(callType.roomId) ?: run { Timber.tag(tag).d("Couldn't find session for incoming call: $activeCall")
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall") return@flatMapLatest flowOf()
return@mapLatest null }
} val room = client.getRoom(callType.roomId) ?: run {
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
return@flatMapLatest flowOf()
}
Timber.tag(tag).d("Found room for ringing call: ${room.roomId}") Timber.tag(tag).d("Found room for ringing call: ${room.roomId}")
val eventId = ringingInfo.notificationData.eventId
room to eventId
}
roomForActiveCallFlow
.flatMapLatest { pair ->
val (room, eventId) = pair
// This will cancel the previous iteration of flatMapLatest if the active call is now null
?: return@flatMapLatest flowOf()
// If we have declined from another phone we want to stop ringing. // If we have declined from another phone we want to stop ringing.
room.subscribeToCallDecline(eventId) room.subscribeToCallDecline(ringingInfo.notificationData.eventId)
.filter { decliner -> .filter { decliner ->
Timber.tag(tag).d("Call: $activeCall was declined by $decliner") Timber.tag(tag).d("Call: $activeCall was declined by $decliner")
// only want to listen if the call was declined from another of my sessions, // only want to listen if the call was declined from another of my sessions,
// (we are ringing for an incoming call in a DM) // (we are ringing for an incoming call in a DM)
decliner == room.sessionId decliner == client.sessionId
} }
} }
.onEach { decliner -> .onEach { decliner ->
Timber.tag(tag).d("Call: $activeCall was declined by user from another session") Timber.tag(tag).d("Call: $activeCall was declined by user from another session")
removeCurrentCall() // Remove the active call and cancel the notification
activeCall.value = null
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after call declined from another session")
activeWakeLock.release()
}
cancelIncomingCallNotification()
} }
.launchIn(coroutineScope) .launchIn(coroutineScope)
// This will observe ringing calls and ensure they're terminated if the room call is cancelled or if the user // 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. // has joined the call from another session.
roomForActiveCallFlow activeCall
.flatMapLatest { pair -> .filterNotNull()
val (room, _) = pair .filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
// This will cancel the previous iteration of flatMapLatest if the active call is now null .flatMapLatest { activeCall ->
?: return@flatMapLatest flowOf() val callType = activeCall.callType as CallType.RoomCall
// Get a flow of updated `hasRoomCall` and `activeRoomCallParticipants` values for the room
// We now observe the room info for changes to the active call state and the call participants val room = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull()?.getRoom(callType.roomId) ?: run {
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
return@flatMapLatest flowOf()
}
room.roomInfoFlow.map { room.roomInfoFlow.map {
val participants = it.activeRoomCallParticipants Timber.tag(tag).d("Has room call status changed for ringing call: ${it.hasRoomCall}")
Timber.tag(tag).d("Room call status changed for ringing call | hasRoomCall: ${it.hasRoomCall} | participants: $participants") it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants)
val userIsInTheCall = room.sessionId in participants
it.hasRoomCall to userIsInTheCall
} }
} }
// Filter out duplicate values // We only want to check if the room active call status changes
.distinctUntilChanged() .distinctUntilChanged()
// Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway) // Skip the first one, we're not interested in it (if the check below passes, it had to be active anyway)
.drop(1) .drop(1)
.onEach { (roomHasActiveCall, userIsInTheCall) -> .onEach { (roomHasActiveCall, userIsInTheCall) ->
if (!roomHasActiveCall) { if (!roomHasActiveCall) {
val notificationData = (activeCall.value?.callState as? CallState.Ringing)?.notificationData // The call was cancelled
removeCurrentCall() timedOutCallJob?.cancel()
incomingCallTimedOut(displayMissedCallNotification = true)
if (notificationData != null) {
displayMissedCallNotification(notificationData)
}
} else if (userIsInTheCall) { } else if (userIsInTheCall) {
removeCurrentCall() // The user joined the call from another session
timedOutCallJob?.cancel()
incomingCallTimedOut(displayMissedCallNotification = false)
} }
} }
.launchIn(coroutineScope) .launchIn(coroutineScope)

View file

@ -28,7 +28,6 @@ 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
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 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
import io.element.android.libraries.matrix.test.A_USER_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.room.FakeBaseRoom import io.element.android.libraries.matrix.test.room.FakeBaseRoom
@ -47,7 +46,6 @@ import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.plantTestTimber import io.element.android.tests.testutils.plantTestTimber
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.mockk import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -333,49 +331,6 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isNull() assertThat(manager.activeCall.value).isNull()
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `observeRingingCalls - declining won't do anything if the call was already cancelled`() = runTest {
val room = FakeBaseRoom().apply {
givenRoomInfo(aRoomInfo())
}
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) })
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val manager = spyk<DefaultActiveCallManager>(
createActiveCallManager(
matrixClientProvider = matrixClientProvider,
notificationManagerCompat = notificationManagerCompat,
)
)
manager.registerIncomingCall(aCallNotificationData())
// Call is active (the other user join the call)
room.givenRoomInfo(aRoomInfo(hasRoomCall = true))
advanceTimeBy(1)
// Call is cancelled by us, hanging up
manager.hungUpCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
advanceTimeBy(1)
verify(exactly = 1) { notificationManagerCompat.cancel(any()) }
verify(exactly = 1) { manager.removeCurrentCall() }
assertThat(manager.activeCall.value).isNull()
assertThat(manager.activeWakeLock?.isHeld).isNull()
// Simulate that another user declined the call
room.givenDecliner(A_USER_ID_2, AN_EVENT_ID)
advanceTimeBy(1)
// Check everything stays the same, no extra call to cancelling notifications
verify(exactly = 1) { notificationManagerCompat.cancel(any()) }
verify(exactly = 1) { manager.removeCurrentCall() }
assertThat(manager.activeWakeLock?.isHeld).isNull()
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest { fun `observeRingingCalls - will do nothing if either the session or the room are not found`() = runTest {