Add WakeLock to dismiss ringing call screen when call is cancelled (#4478)

* Add `WakeLock` to dismiss ringing call screen when call is cancelled

We had already some checks in place to automatically cancel a ringing call notification/screen when the call was no longer active, but the `RoomInfo` updates weren't being processed because the app was 'paused'.

The partial wakelock should ensure these room info updates are handled.

* Add mutual exclusion to `ActiveCallManager` methods to improve thread safety
This commit is contained in:
Jorge Martin Espinosa
2025-04-02 11:51:39 +02:00
committed by GitHub
parent f52f24b464
commit e45151d9d1
12 changed files with 152 additions and 48 deletions

View File

@@ -32,7 +32,7 @@ interface ElementCallEntryPoint {
* @param notificationChannelId The id of the notification channel to use for the call notification.
* @param textContent The text content of the notification. If null the default content from the system will be used.
*/
fun handleIncomingCall(
suspend fun handleIncomingCall(
callType: CallType.RoomCall,
eventId: EventId,
senderId: UserId,

View File

@@ -34,7 +34,7 @@ class DefaultElementCallEntryPoint @Inject constructor(
context.startActivity(IntentProvider.createIntent(context, callType))
}
override fun handleIncomingCall(
override suspend fun handleIncomingCall(
callType: CallType.RoomCall,
eventId: EventId,
senderId: UserId,

View File

@@ -16,6 +16,8 @@ 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.libraries.architecture.bindings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
@@ -27,10 +29,16 @@ class DeclineCallBroadcastReceiver : BroadcastReceiver() {
}
@Inject
lateinit var activeCallManager: ActiveCallManager
@Inject
lateinit var appCoroutineScope: CoroutineScope
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)
activeCallManager.hungUpCall(callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
appCoroutineScope.launch {
activeCallManager.hungUpCall(callType = CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
}
}
}

View File

@@ -62,6 +62,7 @@ class CallScreenPresenter @AssistedInject constructor(
private val activeCallManager: ActiveCallManager,
private val languageTagProvider: LanguageTagProvider,
private val appForegroundStateService: AppForegroundStateService,
private val appCoroutineScope: CoroutineScope,
) : Presenter<CallScreenState> {
@AssistedFactory
interface Factory {
@@ -87,7 +88,7 @@ class CallScreenPresenter @AssistedInject constructor(
coroutineScope.launch {
// Sets the call as joined
activeCallManager.joinedCall(callType)
loadUrl(
fetchRoomCallUrl(
inputs = callType,
urlState = urlState,
callWidgetDriver = callWidgetDriver,
@@ -96,7 +97,7 @@ class CallScreenPresenter @AssistedInject constructor(
)
}
onDispose {
activeCallManager.hungUpCall(callType)
appCoroutineScope.launch { activeCallManager.hungUpCall(callType) }
}
}
@@ -187,7 +188,7 @@ class CallScreenPresenter @AssistedInject constructor(
)
}
private suspend fun loadUrl(
private suspend fun fetchRoomCallUrl(
inputs: CallType,
urlState: MutableState<AsyncData<String>>,
callWidgetDriver: MutableState<MatrixWidgetDriver?>,

View File

@@ -24,9 +24,11 @@ import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
@@ -55,6 +57,9 @@ class IncomingCallActivity : AppCompatActivity() {
@Inject
lateinit var buildMeta: BuildMeta
@Inject
lateinit var appCoroutineScope: CoroutineScope
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -102,6 +107,8 @@ class IncomingCallActivity : AppCompatActivity() {
private fun onCancel() {
val activeCall = activeCallManager.activeCall.value ?: return
activeCallManager.hungUpCall(callType = activeCall.callType)
appCoroutineScope.launch {
activeCallManager.hungUpCall(callType = activeCall.callType)
}
}
}

View File

@@ -8,8 +8,11 @@
package io.element.android.features.call.impl.utils
import android.annotation.SuppressLint
import android.content.Context
import android.os.PowerManager
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.call.api.CallType
@@ -17,6 +20,7 @@ import io.element.android.features.call.api.CurrentCall
import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.notifications.RingingCallNotificationCreator
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.push.api.notifications.ForegroundServiceType
@@ -38,6 +42,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@@ -55,25 +61,26 @@ interface ActiveCallManager {
* Registers an incoming call if there isn't an existing active call and posts a [CallState.Ringing] notification.
* @param notificationData The data for the incoming call notification.
*/
fun registerIncomingCall(notificationData: CallNotificationData)
suspend fun registerIncomingCall(notificationData: CallNotificationData)
/**
* 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(callType: CallType)
suspend fun hungUpCall(callType: CallType)
/**
* Called after the user joined a call. It will remove any existing UI and set the call state as [CallState.InCall].
*
* @param callType The type of call that the user joined, either an external url one or a room one.
*/
fun joinedCall(callType: CallType)
suspend fun joinedCall(callType: CallType)
}
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultActiveCallManager @Inject constructor(
@ApplicationContext context: Context,
private val coroutineScope: CoroutineScope,
private val onMissedCallNotificationHandler: OnMissedCallNotificationHandler,
private val ringingCallNotificationCreator: RingingCallNotificationCreator,
@@ -83,33 +90,47 @@ class DefaultActiveCallManager @Inject constructor(
) : ActiveCallManager {
private var timedOutCallJob: Job? = null
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val activeWakeLock: PowerManager.WakeLock? = context.getSystemService<PowerManager>()
?.takeIf { it.isWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK) }
?.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "${context.packageName}:IncomingCallWakeLock")
override val activeCall = MutableStateFlow<ActiveCall?>(null)
private val mutex = Mutex()
init {
observeRingingCall()
observeCurrentCall()
}
override fun registerIncomingCall(notificationData: CallNotificationData) {
if (activeCall.value != null) {
displayMissedCallNotification(notificationData)
Timber.w("Already have an active call, ignoring incoming call: $notificationData")
return
}
activeCall.value = ActiveCall(
callType = CallType.RoomCall(
sessionId = notificationData.sessionId,
roomId = notificationData.roomId,
),
callState = CallState.Ringing(notificationData),
)
override suspend fun registerIncomingCall(notificationData: CallNotificationData) {
mutex.withLock {
if (activeCall.value != null) {
displayMissedCallNotification(notificationData)
Timber.w("Already have an active call, ignoring incoming call: $notificationData")
return
}
activeCall.value = ActiveCall(
callType = CallType.RoomCall(
sessionId = notificationData.sessionId,
roomId = notificationData.roomId,
),
callState = CallState.Ringing(notificationData),
)
timedOutCallJob = coroutineScope.launch {
showIncomingCallNotification(notificationData)
timedOutCallJob = coroutineScope.launch {
showIncomingCallNotification(notificationData)
// Wait for the ringing call to time out
delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
incomingCallTimedOut(displayMissedCallNotification = true)
// Wait for the ringing call to time out
delay(ElementCallConfig.RINGING_CALL_DURATION_SECONDS.seconds)
incomingCallTimedOut(displayMissedCallNotification = true)
}
// Acquire a wake lock to keep the device awake during the incoming call, so we can process the room info data
if (activeWakeLock?.isHeld == false) {
activeWakeLock.acquire(ElementCallConfig.RINGING_CALL_DURATION_SECONDS * 1000L)
}
}
}
@@ -117,10 +138,13 @@ class DefaultActiveCallManager @Inject constructor(
* 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) {
suspend fun incomingCallTimedOut(displayMissedCallNotification: Boolean) = mutex.withLock {
val previousActiveCall = activeCall.value ?: return
val notificationData = (previousActiveCall.callState as? CallState.Ringing)?.notificationData ?: return
activeCall.value = null
if (activeWakeLock?.isHeld == true) {
activeWakeLock.release()
}
cancelIncomingCallNotification()
@@ -129,18 +153,24 @@ class DefaultActiveCallManager @Inject constructor(
}
}
override fun hungUpCall(callType: CallType) {
override suspend fun hungUpCall(callType: CallType) = mutex.withLock {
if (activeCall.value?.callType != callType) {
Timber.w("Call type $callType does not match the active call type, ignoring")
return
}
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
activeWakeLock.release()
}
timedOutCallJob?.cancel()
activeCall.value = null
}
override fun joinedCall(callType: CallType) {
override suspend fun joinedCall(callType: CallType) = mutex.withLock {
cancelIncomingCallNotification()
if (activeWakeLock?.isHeld == true) {
activeWakeLock.release()
}
timedOutCallJob?.cancel()
activeCall.value = ActiveCall(
@@ -201,6 +231,7 @@ class DefaultActiveCallManager @Inject constructor(
?.getRoom(callType.roomId)
?.roomInfoFlow
?.map {
Timber.d("Has room call status changed for ringing call: ${it.hasRoomCall}")
it.hasRoomCall to (callType.sessionId in it.activeRoomCallParticipants)
}
?: flowOf()

View File

@@ -20,16 +20,21 @@ 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_USER_ID_2
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.Shadows.shadowOf
import kotlin.time.Duration.Companion.seconds
@RunWith(RobolectricTestRunner::class)
class DefaultElementCallEntryPointTest {
@Test
fun `startCall - starts ElementCallActivity setup with the needed extras`() {
fun `startCall - starts ElementCallActivity setup with the needed extras`() = runTest {
val entryPoint = createEntryPoint()
entryPoint.startCall(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID))
@@ -39,8 +44,9 @@ class DefaultElementCallEntryPointTest {
assertThat(intent.extras?.containsKey("EXTRA_CALL_TYPE")).isTrue()
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `handleIncomingCall - registers the incoming call using ActiveCallManager`() {
fun `handleIncomingCall - registers the incoming call using ActiveCallManager`() = runTest {
val registerIncomingCallLambda = lambdaRecorder<CallNotificationData, Unit> {}
val activeCallManager = FakeActiveCallManager(registerIncomingCallResult = registerIncomingCallLambda)
val entryPoint = createEntryPoint(activeCallManager = activeCallManager)
@@ -57,10 +63,12 @@ class DefaultElementCallEntryPointTest {
textContent = "textContent",
)
advanceTimeBy(1.seconds)
registerIncomingCallLambda.assertions().isCalledOnce()
}
private fun createEntryPoint(
private fun TestScope.createEntryPoint(
activeCallManager: FakeActiveCallManager = FakeActiveCallManager(),
) = DefaultElementCallEntryPoint(
context = InstrumentationRegistry.getInstrumentation().targetContext,

View File

@@ -44,12 +44,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.time.Duration.Companion.seconds
class CallScreenPresenterTest {
@OptIn(ExperimentalCoroutinesApi::class) class CallScreenPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@@ -66,7 +68,8 @@ class CallScreenPresenterTest {
presenter.present()
}.test {
// Wait until the URL is loaded
skipItems(1)
advanceTimeBy(1.seconds)
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.urlState).isEqualTo(AsyncData.Success("https://call.element.io"))
assertThat(initialState.webViewError).isNull()
@@ -101,16 +104,23 @@ class CallScreenPresenterTest {
presenter.present()
}.test {
// Wait until the URL is loaded
advanceTimeBy(1.seconds)
skipItems(1)
joinedCallLambda.assertions().isCalledOnce()
val initialState = awaitItem()
assertThat(initialState.urlState).isInstanceOf(AsyncData.Success::class.java)
assertThat(initialState.urlState).isInstanceOf(AsyncData.Loading::class.java)
assertThat(initialState.isCallActive).isFalse()
assertThat(initialState.isInWidgetMode).isTrue()
assertThat(widgetProvider.getWidgetCalled).isTrue()
assertThat(widgetDriver.runCalledCount).isEqualTo(1)
analyticsLambda.assertions().isCalledOnce().with(value(MobileScreen.ScreenName.RoomCall))
sendCallNotificationIfNeededLambda.assertions().isCalledOnce()
// Wait until the WidgetDriver is loaded
skipItems(1)
assertThat(awaitItem().urlState).isInstanceOf(AsyncData.Success::class.java)
}
}
@@ -126,6 +136,9 @@ class CallScreenPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
// Give it time to load the URL and WidgetDriver
advanceTimeBy(1.seconds)
val initialState = awaitItem()
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
@@ -141,7 +154,6 @@ class CallScreenPresenterTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - hang up event closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
val navigator = FakeCallScreenNavigator()
@@ -158,11 +170,15 @@ class CallScreenPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
// Give it time to load the URL and WidgetDriver
advanceTimeBy(1.seconds)
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
initialState.eventSink(CallScreenEvents.Hangup)
// Let background coroutines run
// Let background coroutines run and the widget drive be received
runCurrent()
assertThat(navigator.closeCalled).isTrue()
@@ -172,7 +188,6 @@ class CallScreenPresenterTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - a received close message closes the screen and stops the widget driver`() = runTest(UnconfinedTestDispatcher()) {
val navigator = FakeCallScreenNavigator()
@@ -189,11 +204,16 @@ class CallScreenPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
// Give it time to load the URL and WidgetDriver
advanceTimeBy(1.seconds)
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
messageInterceptor.givenInterceptedMessage("""{"action":"io.element.close","api":"fromWidget","widgetId":"1","requestId":"1"}""")
// Let background coroutines run
advanceTimeBy(1.seconds)
runCurrent()
assertThat(navigator.closeCalled).isTrue()
@@ -218,7 +238,9 @@ class CallScreenPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
skipItems(1)
// Give it time to load the URL and WidgetDriver
advanceTimeBy(1.seconds)
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.isCallActive).isFalse()
initialState.eventSink(CallScreenEvents.SetupMessageChannels(messageInterceptor))
@@ -235,7 +257,7 @@ class CallScreenPresenterTest {
}
""".trimIndent()
)
skipItems(1)
skipItems(2)
val finalState = awaitItem()
assertThat(finalState.isCallActive).isTrue()
}
@@ -300,7 +322,8 @@ class CallScreenPresenterTest {
presenter.present()
}.test {
// Wait until the URL is loaded
skipItems(1)
advanceTimeBy(1.seconds)
skipItems(2)
val initialState = awaitItem()
initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error"))
val finalState = awaitItem()
@@ -329,6 +352,8 @@ class CallScreenPresenterTest {
initialState.eventSink(CallScreenEvents.OnWebViewError("A Webview error"))
val finalState = awaitItem()
assertThat(finalState.webViewError).isNull()
cancelAndIgnoreRemainingEvents()
}
}
@@ -361,6 +386,7 @@ class CallScreenPresenterTest {
screenTracker = screenTracker,
languageTagProvider = FakeLanguageTagProvider("en-US"),
appForegroundStateService = appForegroundStateService,
appCoroutineScope = backgroundScope,
)
}
}

View File

@@ -7,7 +7,9 @@
package io.element.android.features.call.utils
import android.os.PowerManager
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.api.CallType
@@ -49,6 +51,7 @@ import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
@RunWith(RobolectricTestRunner::class)
class DefaultActiveCallManagerTest {
@@ -57,10 +60,12 @@ class DefaultActiveCallManagerTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `registerIncomingCall - sets the incoming call as active`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
inCancellableScope {
val manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
assertThat(manager.activeWakeLock?.isHeld).isFalse()
assertThat(manager.activeCall.value).isNull()
val callNotificationData = aCallNotificationData()
@@ -78,6 +83,7 @@ class DefaultActiveCallManagerTest {
runCurrent()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
verify { notificationManagerCompat.notify(notificationId, any()) }
}
}
@@ -128,6 +134,7 @@ class DefaultActiveCallManagerTest {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `incomingCallTimedOut - when there is an active call removes it and adds a missed call notification`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
val addMissedCallNotificationLambda = lambdaRecorder<SessionId, RoomId, EventId, Unit> { _, _, _ -> }
inCancellableScope {
@@ -138,11 +145,13 @@ class DefaultActiveCallManagerTest {
manager.registerIncomingCall(aCallNotificationData())
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
manager.incomingCallTimedOut(displayMissedCallNotification = true)
advanceTimeBy(1)
assertThat(manager.activeCall.value).isNull()
assertThat(manager.activeWakeLock?.isHeld).isFalse()
addMissedCallNotificationLambda.assertions().isCalledOnce()
verify { notificationManagerCompat.cancel(notificationId) }
}
@@ -150,6 +159,7 @@ class DefaultActiveCallManagerTest {
@Test
fun `hungUpCall - removes existing call if the CallType matches`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
// Create a cancellable coroutine scope to cancel the test when needed
inCancellableScope {
@@ -158,9 +168,11 @@ class DefaultActiveCallManagerTest {
val notificationData = aCallNotificationData()
manager.registerIncomingCall(notificationData)
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
assertThat(manager.activeCall.value).isNull()
assertThat(manager.activeWakeLock?.isHeld).isFalse()
verify { notificationManagerCompat.cancel(notificationId) }
}
@@ -168,6 +180,7 @@ class DefaultActiveCallManagerTest {
@Test
fun `hungUpCall - does nothing if the CallType doesn't match`() = runTest {
setupShadowPowerManager()
val notificationManagerCompat = mockk<NotificationManagerCompat>(relaxed = true)
// Create a cancellable coroutine scope to cancel the test when needed
inCancellableScope {
@@ -175,9 +188,11 @@ class DefaultActiveCallManagerTest {
manager.registerIncomingCall(aCallNotificationData())
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
assertThat(manager.activeCall.value).isNotNull()
assertThat(manager.activeWakeLock?.isHeld).isTrue()
verify(exactly = 0) { notificationManagerCompat.cancel(notificationId) }
}
@@ -284,12 +299,19 @@ class DefaultActiveCallManagerTest {
}
}
private fun setupShadowPowerManager() {
shadowOf(InstrumentationRegistry.getInstrumentation().targetContext.getSystemService<PowerManager>()).apply {
setIsWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK, true)
}
}
private fun CoroutineScope.createActiveCallManager(
matrixClientProvider: FakeMatrixClientProvider = FakeMatrixClientProvider(),
onMissedCallNotificationHandler: FakeOnMissedCallNotificationHandler = FakeOnMissedCallNotificationHandler(),
notificationManagerCompat: NotificationManagerCompat = mockk(relaxed = true),
coroutineScope: CoroutineScope = this,
) = DefaultActiveCallManager(
context = InstrumentationRegistry.getInstrumentation().targetContext,
coroutineScope = coroutineScope,
onMissedCallNotificationHandler = onMissedCallNotificationHandler,
ringingCallNotificationCreator = RingingCallNotificationCreator(

View File

@@ -11,6 +11,7 @@ import io.element.android.features.call.api.CallType
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.ActiveCallManager
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.MutableStateFlow
class FakeActiveCallManager(
@@ -20,15 +21,15 @@ class FakeActiveCallManager(
) : ActiveCallManager {
override val activeCall = MutableStateFlow<ActiveCall?>(null)
override fun registerIncomingCall(notificationData: CallNotificationData) {
override suspend fun registerIncomingCall(notificationData: CallNotificationData) = simulateLongTask {
registerIncomingCallResult(notificationData)
}
override fun hungUpCall(callType: CallType) {
override suspend fun hungUpCall(callType: CallType) = simulateLongTask {
hungUpCallResult(callType)
}
override fun joinedCall(callType: CallType) {
override suspend fun joinedCall(callType: CallType) = simulateLongTask {
joinedCallResult(callType)
}

View File

@@ -30,7 +30,7 @@ class FakeElementCallEntryPoint(
startCallResult(callType)
}
override fun handleIncomingCall(
override suspend fun handleIncomingCall(
callType: CallType.RoomCall,
eventId: EventId,
senderId: UserId,

View File

@@ -118,7 +118,7 @@ class DefaultPushHandler @Inject constructor(
}
}
private fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
private suspend fun handleRingingCallEvent(notifiableEvent: NotifiableRingingCallEvent) {
Timber.i("## handleInternal() : Incoming call.")
elementCallEntryPoint.handleIncomingCall(
callType = CallType.RoomCall(notifiableEvent.sessionId, notifiableEvent.roomId),