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.
This commit is contained in:
parent
93ccf07ad4
commit
fcbc095064
9 changed files with 129 additions and 87 deletions
1
changelog.d/3029.misc
Normal file
1
changelog.d/3029.misc
Normal file
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
loadUrl(callType, urlState, callWidgetDriver)
|
coroutineScope.launch {
|
||||||
|
// Sets the call as joined
|
||||||
if (callType is CallType.RoomCall) {
|
activeCallManager.joinedCall(callType)
|
||||||
activeCallManager.joinedCall(callType.sessionId, callType.roomId)
|
loadUrl(callType, urlState, callWidgetDriver)
|
||||||
|
}
|
||||||
|
onDispose {
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
sessionId = notificationData.sessionId,
|
callType = CallType.RoomCall(
|
||||||
roomId = notificationData.roomId,
|
sessionId = notificationData.sessionId,
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
sessionId = callNotificationData.sessionId,
|
callType = CallType.RoomCall(
|
||||||
roomId = callNotificationData.roomId,
|
sessionId = callNotificationData.sessionId,
|
||||||
|
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 manager = createActiveCallManager(notificationManagerCompat = notificationManagerCompat)
|
||||||
|
|
||||||
|
val notificationData = aCallNotificationData()
|
||||||
|
manager.registerIncomingCall(notificationData)
|
||||||
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
|
manager.hungUpCall(CallType.RoomCall(notificationData.sessionId, notificationData.roomId))
|
||||||
|
assertThat(manager.activeCall.value).isNull()
|
||||||
|
|
||||||
|
verify { notificationManagerCompat.cancel(notificationId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `hungUpCall - does nothing if the CallType doesn't match`() = 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())
|
manager.registerIncomingCall(aCallNotificationData())
|
||||||
assertThat(manager.activeCall.value).isNotNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
manager.hungUpCall()
|
manager.hungUpCall(CallType.ExternalUrl("https://example.com"))
|
||||||
assertThat(manager.activeCall.value).isNull()
|
assertThat(manager.activeCall.value).isNotNull()
|
||||||
|
|
||||||
verify { notificationManagerCompat.cancel(notificationId) }
|
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(
|
||||||
sessionId = A_SESSION_ID,
|
callType = CallType.RoomCall(
|
||||||
roomId = A_ROOM_ID,
|
sessionId = A_SESSION_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,
|
||||||
|
|
|
||||||
|
|
@ -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…
Add table
Add a link
Reference in a new issue