Call: RTC decline event support

This commit is contained in:
Valere
2025-09-09 09:59:40 +02:00
parent 0a16b86569
commit 4e8e355f27
5 changed files with 192 additions and 1 deletions

View File

@@ -186,6 +186,15 @@ class DefaultActiveCallManager(
}
Timber.tag(tag).d("Hung up call: $callType")
if (activeCall.value?.callState is CallState.Ringing) {
val ringing = activeCall.value!!.callState as CallState.Ringing
// Decline the call
matrixClientProvider.getOrRestore(ringing.notificationData.sessionId).getOrNull()?.let { client ->
client.getRoom(ringing.notificationData.roomId)?.let { room ->
room.declineCall(ringing.notificationData.eventId)
}
}
}
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
@@ -256,6 +265,42 @@ class DefaultActiveCallManager(
@OptIn(ExperimentalCoroutinesApi::class)
private fun observeRingingCall() {
activeCall
.filterNotNull()
.filter { it.callState is CallState.Ringing && it.callType is CallType.RoomCall }
.flatMapLatest { activeCall ->
val callType = activeCall.callType as CallType.RoomCall
val ringingInfo = activeCall.callState as CallState.Ringing
val client = matrixClientProvider.getOrRestore(callType.sessionId).getOrNull() ?: run {
Timber.tag(tag).d("Couldn't find room for incoming call: $activeCall")
return@flatMapLatest flowOf()
}
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 rining call: ${room.roomId}")
// If we have declined from another phone we want to stop ringing.
room.subscribeToCallDecline(ringingInfo.notificationData.eventId)
.filter { 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,
// (we are ringing for an incoming call in a DM)
decliner == client.sessionId
}
}.onEach { decliner ->
Timber.tag(tag).d("Call: $activeCall was declined by from another session")
// decline
activeCall.value = null
if (activeWakeLock?.isHeld == true) {
Timber.tag(tag).d("Releasing partial wakelock after timeout")
activeWakeLock.release()
}
cancelIncomingCallNotification()
}
.launchIn(coroutineScope)
// 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

View File

@@ -22,13 +22,16 @@ import io.element.android.features.call.test.aCallNotificationData
import io.element.android.libraries.matrix.api.core.EventId
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.room.JoinedRoom
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.FakeMatrixClient
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.FakeJoinedRoom
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
import io.element.android.libraries.push.api.notifications.NotificationIdProvider
@@ -38,6 +41,7 @@ import io.element.android.libraries.push.test.notifications.push.FakeNotificatio
import io.element.android.services.appnavstate.test.FakeAppForegroundStateService
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.mockk.coVerify
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -49,6 +53,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import timber.log.Timber
@RunWith(RobolectricTestRunner::class)
class DefaultActiveCallManagerTest {
@@ -164,6 +169,107 @@ class DefaultActiveCallManagerTest {
verify { notificationManagerCompat.cancel(notificationId) }
}
@Test
fun `Decline event - Hangup on a ringing call should send a decline event`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val room = mockk<JoinedRoom>(relaxed = true)
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
val manager = createActiveCallManager(
matrixClientProvider = clientProvider,
notificationManagerCompat = notificationManagerCompat
)
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
manager.registerIncomingCall(notificationData)
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
coVerify {
room.declineCall(notificationEventId = notificationData.eventId)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `Decline event - Declining from another session should stop ringing`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val room = FakeJoinedRoom()
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
val manager = createActiveCallManager(
matrixClientProvider = clientProvider,
notificationManagerCompat = notificationManagerCompat
)
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
manager.registerIncomingCall(notificationData)
runCurrent()
// Simulate declined from other session
room.baseRoom.givenDecliner(matrixClient.sessionId, notificationData.eventId)
runCurrent()
assertThat(manager.activeCall.value).isNull()
assertThat(manager.activeWakeLock?.isHeld).isFalse()
verify { notificationManagerCompat.cancel(notificationId) }
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `Decline event - Should ignore decline for other notification events`() = runTest {
Timber.plant(object : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
println("$tag: $message")
}
})
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val room = FakeJoinedRoom()
val matrixClient = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val clientProvider = FakeMatrixClientProvider({ Result.success(matrixClient) })
val manager = createActiveCallManager(
matrixClientProvider = clientProvider,
notificationManagerCompat = notificationManagerCompat
)
val notificationData = aCallNotificationData(roomId = A_ROOM_ID)
manager.registerIncomingCall(notificationData)
runCurrent()
// Simulate declined for another notification event
room.baseRoom.givenDecliner(matrixClient.sessionId, AN_EVENT_ID_2)
runCurrent()
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
}
@Test
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
setupShadowPowerManager()