Browse Source

Improve how active calls work (#3029)

* Improve how active calls work:

- Sending the `m.call.notify` event is now done in `CallScreenPresenter` once we know the sync is running.
- You can mark a call of both external url or room type as joined.
- Hanging up checks the current active call type and will only remove it if it matches.
pull/3046/head
Jorge Martin Espinosa 3 months ago committed by GitHub
parent
commit
2e32adf1f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      changelog.d/3029.misc
  2. 20
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt
  3. 10
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt
  4. 43
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt
  5. 3
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
  6. 42
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt
  7. 22
      features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt
  8. 40
      features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt
  9. 15
      features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt

1
changelog.d/3029.misc

@ -0,0 +1 @@
Improve how active calls work by also taking into account external url calls and waiting for the sync process to start before sending the `m.call.notify` event.

20
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/notifications/RingingCallNotificationCreator.kt

@ -84,11 +84,24 @@ class RingingCallNotificationCreator @Inject constructor(
.build() .build()
val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId)) val answerIntent = IntentProvider.getPendingIntent(context, CallType.RoomCall(sessionId, roomId))
val notificationData = CallNotificationData(
sessionId = sessionId,
roomId = roomId,
eventId = eventId,
senderId = senderId,
roomName = roomName,
senderName = senderDisplayName,
avatarUrl = roomAvatarUrl,
notificationChannelId = notificationChannelId,
timestamp = timestamp
)
val declineIntent = PendingIntentCompat.getBroadcast( val declineIntent = PendingIntentCompat.getBroadcast(
context, context,
DECLINE_REQUEST_CODE, DECLINE_REQUEST_CODE,
Intent(context, DeclineCallBroadcastReceiver::class.java), Intent(context, DeclineCallBroadcastReceiver::class.java).apply {
putExtra(DeclineCallBroadcastReceiver.EXTRA_NOTIFICATION_DATA, notificationData)
},
PendingIntent.FLAG_CANCEL_CURRENT, PendingIntent.FLAG_CANCEL_CURRENT,
false, false,
)!! )!!
@ -97,10 +110,7 @@ class RingingCallNotificationCreator @Inject constructor(
context, context,
FULL_SCREEN_INTENT_REQUEST_CODE, FULL_SCREEN_INTENT_REQUEST_CODE,
Intent(context, IncomingCallActivity::class.java).apply { Intent(context, IncomingCallActivity::class.java).apply {
putExtra( putExtra(IncomingCallActivity.EXTRA_NOTIFICATION_DATA, notificationData)
IncomingCallActivity.EXTRA_NOTIFICATION_DATA,
CallNotificationData(sessionId, roomId, eventId, senderId, roomName, senderDisplayName, roomAvatarUrl, notificationChannelId, timestamp)
)
}, },
PendingIntent.FLAG_CANCEL_CURRENT, PendingIntent.FLAG_CANCEL_CURRENT,
false false

10
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/receivers/DeclineCallBroadcastReceiver.kt

@ -19,7 +19,10 @@ package io.element.android.features.call.impl.receivers
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.IntentCompat
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.di.CallBindings
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.utils.ActiveCallManager import io.element.android.features.call.impl.utils.ActiveCallManager
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import javax.inject.Inject import javax.inject.Inject
@ -28,10 +31,15 @@ import javax.inject.Inject
* Broadcast receiver to decline the incoming call. * Broadcast receiver to decline the incoming call.
*/ */
class DeclineCallBroadcastReceiver : BroadcastReceiver() { class DeclineCallBroadcastReceiver : BroadcastReceiver() {
companion object {
const val EXTRA_NOTIFICATION_DATA = "EXTRA_NOTIFICATION_DATA"
}
@Inject @Inject
lateinit var activeCallManager: ActiveCallManager lateinit var activeCallManager: ActiveCallManager
override fun onReceive(context: Context, intent: Intent?) { override fun onReceive(context: Context, intent: Intent?) {
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
?: return
context.bindings<CallBindings>().inject(this) context.bindings<CallBindings>().inject(this)
activeCallManager.hungUpCall() activeCallManager.hungUpCall(callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
} }
} }

43
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/CallScreenPresenter.kt

@ -40,14 +40,15 @@ import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -75,6 +76,7 @@ class CallScreenPresenter @AssistedInject constructor(
private val isInWidgetMode = callType is CallType.RoomCall private val isInWidgetMode = callType is CallType.RoomCall
private val userAgent = userAgentProvider.provide() private val userAgent = userAgentProvider.provide()
private var notifiedCallStart = false
@Composable @Composable
override fun present(): CallScreenState { override fun present(): CallScreenState {
@ -84,11 +86,14 @@ class CallScreenPresenter @AssistedInject constructor(
val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) } val messageInterceptor = remember { mutableStateOf<WidgetMessageInterceptor?>(null) }
var isJoinedCall by rememberSaveable { mutableStateOf(false) } var isJoinedCall by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) { DisposableEffect(Unit) {
coroutineScope.launch {
// Sets the call as joined
activeCallManager.joinedCall(callType)
loadUrl(callType, urlState, callWidgetDriver) loadUrl(callType, urlState, callWidgetDriver)
}
if (callType is CallType.RoomCall) { onDispose {
activeCallManager.joinedCall(callType.sessionId, callType.roomId) activeCallManager.hungUpCall(callType)
} }
} }
@ -140,14 +145,6 @@ class CallScreenPresenter @AssistedInject constructor(
} }
} }
DisposableEffect(Unit) {
onDispose {
if (callType is CallType.RoomCall) {
activeCallManager.hungUpCall()
}
}
}
fun handleEvents(event: CallScreenEvents) { fun handleEvents(event: CallScreenEvents) {
when (event) { when (event) {
is CallScreenEvents.Hangup -> { is CallScreenEvents.Hangup -> {
@ -173,15 +170,15 @@ class CallScreenPresenter @AssistedInject constructor(
urlState = urlState.value, urlState = urlState.value,
userAgent = userAgent, userAgent = userAgent,
isInWidgetMode = isInWidgetMode, isInWidgetMode = isInWidgetMode,
eventSink = ::handleEvents, eventSink = { handleEvents(it) },
) )
} }
private fun CoroutineScope.loadUrl( private suspend fun loadUrl(
inputs: CallType, inputs: CallType,
urlState: MutableState<AsyncData<String>>, urlState: MutableState<AsyncData<String>>,
callWidgetDriver: MutableState<MatrixWidgetDriver?>, callWidgetDriver: MutableState<MatrixWidgetDriver?>,
) = launch { ) {
urlState.runCatchingUpdatingState { urlState.runCatchingUpdatingState {
when (inputs) { when (inputs) {
is CallType.ExternalUrl -> { is CallType.ExternalUrl -> {
@ -209,12 +206,13 @@ class CallScreenPresenter @AssistedInject constructor(
} ?: return@DisposableEffect onDispose { } } ?: return@DisposableEffect onDispose { }
coroutineScope.launch { coroutineScope.launch {
client.syncService().syncState client.syncService().syncState
.onEach { state -> .collect { state ->
if (state != SyncState.Running) { if (state == SyncState.Running) {
client.notifyCallStartIfNeeded(callType.roomId)
} else {
client.syncService().startSync() client.syncService().startSync()
} }
} }
.collect()
} }
onDispose { onDispose {
// We can't use the local coroutine scope here because it will be disposed before this effect // We can't use the local coroutine scope here because it will be disposed before this effect
@ -229,6 +227,13 @@ class CallScreenPresenter @AssistedInject constructor(
} }
} }
private suspend fun MatrixClient.notifyCallStartIfNeeded(roomId: RoomId) {
if (!notifiedCallStart) {
getRoom(roomId)?.sendCallNotificationIfNeeded()
?.onSuccess { notifiedCallStart = true }
}
}
private fun parseMessage(message: String): WidgetMessage? { private fun parseMessage(message: String): WidgetMessage? {
return WidgetMessageSerializer.deserialize(message).getOrNull() return WidgetMessageSerializer.deserialize(message).getOrNull()
} }

3
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt

@ -91,6 +91,7 @@ class IncomingCallActivity : AppCompatActivity() {
} }
private fun onCancel() { private fun onCancel() {
activeCallManager.hungUpCall() val activeCall = activeCallManager.activeCall.value ?: return
activeCallManager.hungUpCall(callType = activeCall.callType)
} }
} }

42
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ActiveCallManager.kt

@ -20,13 +20,11 @@ import android.annotation.SuppressLint
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.ElementCallConfig import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
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
import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler import io.element.android.libraries.push.api.notifications.OnMissedCallNotificationHandler
@ -61,24 +59,23 @@ interface ActiveCallManager {
fun incomingCallTimedOut() fun incomingCallTimedOut()
/** /**
* Hangs up the active call and removes any associated UI. * 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.
*/ */
fun hungUpCall() fun hungUpCall(callType: CallType)
/** /**
* Called when the user joins a call. It will remove any existing UI and set the call state as [CallState.InCall]. * Called after the user joined a call. It will remove any existing UI and set the call state as [CallState.InCall].
* *
* @param sessionId The session ID of the user joining the call. * @param callType The type of call that the user joined, either an external url one or a room one.
* @param roomId The room ID of the call.
*/ */
fun joinedCall(sessionId: SessionId, roomId: RoomId) fun joinedCall(callType: CallType)
} }
@SingleIn(AppScope::class) @SingleIn(AppScope::class)
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultActiveCallManager @Inject constructor( class DefaultActiveCallManager @Inject constructor(
private val coroutineScope: CoroutineScope, private val coroutineScope: CoroutineScope,
private val matrixClientProvider: MatrixClientProvider,
private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler, private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler,
private val ringingCallNotificationCreator: RingingCallNotificationCreator, private val ringingCallNotificationCreator: RingingCallNotificationCreator,
private val notificationManagerCompat: NotificationManagerCompat, private val notificationManagerCompat: NotificationManagerCompat,
@ -94,15 +91,17 @@ class DefaultActiveCallManager @Inject constructor(
return return
} }
activeCall.value = ActiveCall( activeCall.value = ActiveCall(
callType = CallType.RoomCall(
sessionId = notificationData.sessionId, sessionId = notificationData.sessionId,
roomId = notificationData.roomId, roomId = notificationData.roomId,
),
callState = CallState.Ringing(notificationData), callState = CallState.Ringing(notificationData),
) )
timedOutCallJob = coroutineScope.launch { timedOutCallJob = coroutineScope.launch {
showIncomingCallNotification(notificationData) showIncomingCallNotification(notificationData)
// Wait for the call to end // Wait for the ringing call to time out
delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds) delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
incomingCallTimedOut() incomingCallTimedOut()
} }
@ -118,28 +117,24 @@ class DefaultActiveCallManager @Inject constructor(
displayMissedCallNotification(notificationData) displayMissedCallNotification(notificationData)
} }
override fun hungUpCall() { override fun hungUpCall(callType: CallType) {
if (activeCall.value?.callType != callType) {
Timber.w("Call type $callType does not match the active call type, ignoring")
return
}
cancelIncomingCallNotification() cancelIncomingCallNotification()
timedOutCallJob?.cancel() timedOutCallJob?.cancel()
activeCall.value = null activeCall.value = null
} }
override fun joinedCall(sessionId: SessionId, roomId: RoomId) { override fun joinedCall(callType: CallType) {
cancelIncomingCallNotification() cancelIncomingCallNotification()
timedOutCallJob?.cancel() timedOutCallJob?.cancel()
activeCall.value = ActiveCall( activeCall.value = ActiveCall(
sessionId = sessionId, callType = callType,
roomId = roomId,
callState = CallState.InCall, callState = CallState.InCall,
) )
// Send call notification to the room
coroutineScope.launch {
matrixClientProvider.getOrRestore(sessionId)
.getOrNull()
?.getRoom(roomId)
?.sendCallNotificationIfNeeded()
}
} }
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
@ -184,8 +179,7 @@ class DefaultActiveCallManager @Inject constructor(
* Represents an active call. * Represents an active call.
*/ */
data class ActiveCall( data class ActiveCall(
val sessionId: SessionId, val callType: CallType,
val roomId: RoomId,
val callState: CallState, val callState: CallState,
) )

22
features/call/impl/src/test/kotlin/io/element/android/features/call/ui/CallScreenPresenterTest.kt

@ -35,6 +35,7 @@ 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_SESSION_ID
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.FakeMatrixRoom
import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver import io.element.android.libraries.matrix.test.widget.FakeMatrixWidgetDriver
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.services.analytics.api.ScreenTracker import io.element.android.services.analytics.api.ScreenTracker
@ -61,11 +62,13 @@ class CallScreenPresenterTest {
val warmUpRule = WarmUpRule() val warmUpRule = WarmUpRule()
@Test @Test
fun `present - with CallType ExternalUrl just loads the URL`() = runTest { fun `present - with CallType ExternalUrl just loads the URL and sets the call as active`() = runTest {
val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {} val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {}
val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
callType = CallType.ExternalUrl("https://call.element.io"), callType = CallType.ExternalUrl("https://call.element.io"),
screenTracker = FakeScreenTracker(analyticsLambda) screenTracker = FakeScreenTracker(analyticsLambda),
activeCallManager = FakeActiveCallManager(joinedCallResult = joinedCallLambda),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -76,25 +79,35 @@ class CallScreenPresenterTest {
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io")) assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
assertThat(initialState.isInWidgetMode).isFalse() assertThat(initialState.isInWidgetMode).isFalse()
analyticsLambda.assertions().isNeverCalled() analyticsLambda.assertions().isNeverCalled()
joinedCallLambda.assertions().isCalledOnce()
} }
} }
@Test @Test
fun `present - with CallType RoomCall loads URL and runs WidgetDriver`() = runTest { fun `present - with CallType RoomCall sets call as active, loads URL, runs WidgetDriver and notifies the other clients a call started`() = runTest {
val sendCallNotificationIfNeededLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val fakeRoom = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotificationIfNeededLambda)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, fakeRoom)
}
val widgetDriver = FakeMatrixWidgetDriver() val widgetDriver = FakeMatrixWidgetDriver()
val widgetProvider = FakeCallWidgetProvider(widgetDriver) val widgetProvider = FakeCallWidgetProvider(widgetDriver)
val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {} val analyticsLambda = lambdaRecorder<MobileScreen.ScreenName, Unit> {}
val joinedCallLambda = lambdaRecorder<CallType, Unit> {}
val presenter = createCallScreenPresenter( val presenter = createCallScreenPresenter(
matrixClientsProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID), callType = CallType.RoomCall(A_SESSION_ID, A_ROOM_ID),
widgetDriver = widgetDriver, widgetDriver = widgetDriver,
widgetProvider = widgetProvider, widgetProvider = widgetProvider,
screenTracker = FakeScreenTracker(analyticsLambda) screenTracker = FakeScreenTracker(analyticsLambda),
activeCallManager = FakeActiveCallManager(joinedCallResult = joinedCallLambda),
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
// Wait until the URL is loaded // Wait until the URL is loaded
skipItems(1) skipItems(1)
joinedCallLambda.assertions().isCalledOnce()
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java) assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.isInWidgetMode).isTrue() assertThat(initialState.isInWidgetMode).isTrue()
@ -106,6 +119,7 @@ class CallScreenPresenterTest {
listOf(value(MobileScreen.ScreenName.RoomCall)), listOf(value(MobileScreen.ScreenName.RoomCall)),
listOf(value(MobileScreen.ScreenName.RoomCall)) listOf(value(MobileScreen.ScreenName.RoomCall))
) )
sendCallNotificationIfNeededLambda.assertions().isCalledOnce()
} }
} }

40
features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultActiveCallManagerTest.kt

@ -19,6 +19,7 @@ package io.element.android.features.call.utils
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.ActiveCall
import io.element.android.features.call.impl.utils.CallState import io.element.android.features.call.impl.utils.CallState
@ -31,9 +32,7 @@ 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_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.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.FakeMatrixRoom
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
import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder import io.element.android.libraries.push.test.notifications.FakeImageLoaderHolder
@ -69,8 +68,10 @@ class DefaultActiveCallManagerTest {
assertThat(manager.activeCall.value).isEqualTo( assertThat(manager.activeCall.value).isEqualTo(
ActiveCall( ActiveCall(
callType = CallType.RoomCall(
sessionId = callNotificationData.sessionId, sessionId = callNotificationData.sessionId,
roomId = callNotificationData.roomId, roomId = callNotificationData.roomId,
),
callState = CallState.Ringing(callNotificationData) callState = CallState.Ringing(callNotificationData)
) )
) )
@ -98,7 +99,7 @@ class DefaultActiveCallManagerTest {
manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2)) manager.registerIncomingCall(aCallNotificationData(roomId = A_ROOM_ID_2))
assertThat(manager.activeCall.value).isEqualTo(activeCall) assertThat(manager.activeCall.value).isEqualTo(activeCall)
assertThat(manager.activeCall.value?.roomId).isNotEqualTo(A_ROOM_ID_2) assertThat((manager.activeCall.value?.callType as? CallType.RoomCall)?.roomId).isNotEqualTo(A_ROOM_ID_2)
advanceTimeBy(1) advanceTimeBy(1)
@ -141,46 +142,56 @@ class DefaultActiveCallManagerTest {
} }
@Test @Test
fun `hungUpCall - removes existing call`() = runTest { fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true) val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat) val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
manager.registerIncomingCall(aCallNotificationData()) val notificationData = aCallNotificationData()
manager.registerIncomingCall(notificationData)
assertThat(manager.activeCall.value).isNotNull() assertThat(manager.activeCall.value).isNotNull()
manager.hungUpCall() manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
assertThat(manager.activeCall.value).isNull() assertThat(manager.activeCall.value).isNull()
verify { notificationManagerCompat.cancel(notificationId) } verify { notificationManagerCompat.cancel(notificationId) }
} }
@Test
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
manager.registerIncomingCall(aCallNotificationData())
assertThat(manager.activeCall.value).isNotNull()
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
assertThat(manager.activeCall.value).isNotNull()
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
}
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest { fun `joinedCall - register an ongoing call and tries sending the call notify event`() = runTest {
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true) val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val sendCallNotifyLambda = lambdaRecorder<Result<Unit>> { Result.success(Unit) }
val room = FakeMatrixRoom(sendCallNotificationIfNeededResult = sendCallNotifyLambda)
val client = FakeMatrixClient().apply {
givenGetRoomResult(A_ROOM_ID, room)
}
val manager = createActiveCallManager( val manager = createActiveCallManager(
matrixClientProvider = FakeMatrixClientProvider(getClient = { Result.success(client) }),
notificationManagerCompat = notificationManagerCompat, notificationManagerCompat = notificationManagerCompat,
) )
assertThat(manager.activeCall.value).isNull() assertThat(manager.activeCall.value).isNull()
manager.joinedCall(A_SESSION_ID, A_ROOM_ID) manager.joinedCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
assertThat(manager.activeCall.value).isEqualTo( assertThat(manager.activeCall.value).isEqualTo(
ActiveCall( ActiveCall(
callType = CallType.RoomCall(
sessionId = A_SESSION_ID, sessionId = A_SESSION_ID,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
),
callState = CallState.InCall, callState = CallState.InCall,
) )
) )
runCurrent() runCurrent()
sendCallNotifyLambda.assertions().isCalledOnce()
verify { notificationManagerCompat.cancel(notificationId) } verify { notificationManagerCompat.cancel(notificationId) }
} }
@ -190,7 +201,6 @@ class DefaultActiveCallManagerTest {
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true), notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
) = DefaultActiveCallManager( ) = DefaultActiveCallManager(
coroutineScope = this, coroutineScope = this,
matrixClientProvider = matrixClientProvider,
onMissedCallNotificationHandler = onMissedCallNotificationHandler, onMissedCallNotificationHandler = onMissedCallNotificationHandler,
ringingCallNotificationCreator = RingingCallNotificationCreator( ringingCallNotificationCreator = RingingCallNotificationCreator(
context = InstrumentationRegistry.getInstrumentation().targetContext, context = InstrumentationRegistry.getInstrumentation().targetContext,

15
features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeActiveCallManager.kt

@ -16,18 +16,17 @@
package io.element.android.features.call.utils package io.element.android.features.call.utils
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.notifications.CallNotificationData import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.utils.ActiveCall import io.element.android.features.call.impl.utils.ActiveCall
import io.element.android.features.call.impl.utils.ActiveCallManager import io.element.android.features.call.impl.utils.ActiveCallManager
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class FakeActiveCallManager( class FakeActiveCallManager(
var registerIncomingCallResult: (CallNotificationData) -> Unit = {}, var registerIncomingCallResult: (CallNotificationData) -> Unit = {},
var incomingCallTimedOutResult: () -> Unit = {}, var incomingCallTimedOutResult: () -> Unit = {},
var hungUpCallResult: () -> Unit = {}, var hungUpCallResult: (CallType) -> Unit = {},
var joinedCallResult: (SessionId, RoomId) -> Unit = { _, _ -> }, var joinedCallResult: (CallType) -> Unit = {},
) : ActiveCallManager { ) : ActiveCallManager {
override val activeCall = MutableStateFlow<ActiveCall?>(null) override val activeCall = MutableStateFlow<ActiveCall?>(null)
@ -39,12 +38,12 @@ class FakeActiveCallManager(
incomingCallTimedOutResult() incomingCallTimedOutResult()
} }
override fun hungUpCall() { override fun hungUpCall(callType: CallType) {
hungUpCallResult() hungUpCallResult(callType)
} }
override fun joinedCall(sessionId: SessionId, roomId: RoomId) { override fun joinedCall(callType: CallType) {
joinedCallResult(sessionId, roomId) joinedCallResult(callType)
} }
fun setActiveCall(value: ActiveCall?) { fun setActiveCall(value: ActiveCall?) {

Loading…
Cancel
Save