Ensure the user can join the call even if they has joined a call in another session.

This commit is contained in:
Benoit Marty 2024-11-06 09:43:38 +01:00
parent 2a35edb14a
commit 0bbb1ac23d
10 changed files with 116 additions and 1 deletions

View file

@ -0,0 +1,25 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.call.api
import io.element.android.libraries.matrix.api.core.RoomId
/**
* Value for the local current call.
*/
sealed interface CurrentCall {
data object None : CurrentCall
data class RoomCall(
val roomId: RoomId,
) : CurrentCall
data class ExternalUrl(
val url: String,
) : CurrentCall
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.call.api
import kotlinx.coroutines.flow.StateFlow
interface CurrentCallObserver {
/**
* The current call state flow, which will be updated when the active call changes.
* This value reflect the local state of the call. It is not updated if the user answers
* a call from another session.
*/
val currentCall: StateFlow<CurrentCall>
}

View file

@ -12,6 +12,7 @@ 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.api.CallType
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.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
@ -82,6 +83,7 @@ class DefaultActiveCallManager @Inject constructor(
private val ringingCallNotificationCreator: RingingCallNotificationCreator, private val ringingCallNotificationCreator: RingingCallNotificationCreator,
private val notificationManagerCompat: NotificationManagerCompat, private val notificationManagerCompat: NotificationManagerCompat,
private val matrixClientProvider: MatrixClientProvider, private val matrixClientProvider: MatrixClientProvider,
private val defaultCurrentCallObserver: DefaultCurrentCallObserver,
) : ActiveCallManager { ) : ActiveCallManager {
private var timedOutCallJob: Job? = null private var timedOutCallJob: Job? = null
@ -89,6 +91,7 @@ class DefaultActiveCallManager @Inject constructor(
init { init {
observeRingingCall() observeRingingCall()
observeCurrentCall()
} }
override fun registerIncomingCall(notificationData: CallNotificationData) { override fun registerIncomingCall(notificationData: CallNotificationData) {
@ -209,6 +212,28 @@ class DefaultActiveCallManager @Inject constructor(
} }
.launchIn(coroutineScope) .launchIn(coroutineScope)
} }
private fun observeCurrentCall() {
activeCall
.onEach { value ->
if (value == null) {
defaultCurrentCallObserver.onCallEnded()
} else {
when (value.callState) {
is CallState.Ringing -> {
// Nothing to do
}
is CallState.InCall -> {
when (val callType = value.callType) {
is CallType.ExternalUrl -> defaultCurrentCallObserver.onCallStarted(CurrentCall.ExternalUrl(callType.url))
is CallType.RoomCall -> defaultCurrentCallObserver.onCallStarted(CurrentCall.RoomCall(callType.roomId))
}
}
}
}
}
.launchIn(coroutineScope)
}
} }
/** /**

View file

@ -0,0 +1,30 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.call.impl.utils
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.call.api.CurrentCall
import io.element.android.features.call.api.CurrentCallObserver
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import kotlinx.coroutines.flow.MutableStateFlow
import javax.inject.Inject
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultCurrentCallObserver @Inject constructor() : CurrentCallObserver {
override val currentCall = MutableStateFlow<CurrentCall>(CurrentCall.None)
fun onCallStarted(call: CurrentCall) {
currentCall.value = call
}
fun onCallEnded() {
currentCall.value = CurrentCall.None
}
}

View file

@ -15,6 +15,7 @@ import io.element.android.features.call.impl.notifications.RingingCallNotificati
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
import io.element.android.features.call.impl.utils.DefaultActiveCallManager import io.element.android.features.call.impl.utils.DefaultActiveCallManager
import io.element.android.features.call.impl.utils.DefaultCurrentCallObserver
import io.element.android.features.call.test.aCallNotificationData 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.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
@ -299,5 +300,6 @@ class DefaultActiveCallManagerTest {
), ),
notificationManagerCompat = notificationManagerCompat, notificationManagerCompat = notificationManagerCompat,
matrixClientProvider = matrixClientProvider, matrixClientProvider = matrixClientProvider,
defaultCurrentCallObserver = DefaultCurrentCallObserver(),
) )
} }

View file

@ -79,7 +79,7 @@ private fun OnGoingCallMenuItem(
onJoinCallClick: () -> Unit, onJoinCallClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
if (!roomCallState.isUserInTheCall) { if (!roomCallState.isUserLocallyInTheCall) {
Button( Button(
onClick = onJoinCallClick, onClick = onJoinCallClick,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(

View file

@ -20,6 +20,7 @@ sealed interface RoomCallState {
data class OnGoing( data class OnGoing(
val canJoinCall: Boolean, val canJoinCall: Boolean,
val isUserInTheCall: Boolean, val isUserInTheCall: Boolean,
val isUserLocallyInTheCall: Boolean,
) : RoomCallState ) : RoomCallState
} }

View file

@ -22,9 +22,11 @@ open class RoomCallStateProvider : PreviewParameterProvider<RoomCallState> {
fun anOngoingCallState( fun anOngoingCallState(
canJoinCall: Boolean = true, canJoinCall: Boolean = true,
isUserInTheCall: Boolean = false, isUserInTheCall: Boolean = false,
isUserLocallyInTheCall: Boolean = isUserInTheCall,
) = RoomCallState.OnGoing( ) = RoomCallState.OnGoing(
canJoinCall = canJoinCall, canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall, isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall,
) )
fun aStandByCallState( fun aStandByCallState(

View file

@ -20,6 +20,7 @@ setupAnvil()
dependencies { dependencies {
api(projects.features.roomcall.api) api(projects.features.roomcall.api)
implementation(libs.kotlinx.collections.immutable) implementation(libs.kotlinx.collections.immutable)
implementation(projects.features.call.api)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui) implementation(projects.libraries.matrixui)

View file

@ -12,6 +12,8 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import io.element.android.features.call.api.CurrentCall
import io.element.android.features.call.api.CurrentCallObserver
import io.element.android.features.roomcall.api.RoomCallState import io.element.android.features.roomcall.api.RoomCallState
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -20,6 +22,7 @@ import javax.inject.Inject
class RoomCallStatePresenter @Inject constructor( class RoomCallStatePresenter @Inject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val currentCallObserver: CurrentCallObserver,
) : Presenter<RoomCallState> { ) : Presenter<RoomCallState> {
@Composable @Composable
override fun present(): RoomCallState { override fun present(): RoomCallState {
@ -31,10 +34,17 @@ class RoomCallStatePresenter @Inject constructor(
room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty() room.sessionId in roomInfo?.activeRoomCallParticipants.orEmpty()
} }
} }
val currentCall by currentCallObserver.currentCall.collectAsState()
val isUserLocallyInTheCall by remember {
derivedStateOf {
(currentCall as? CurrentCall.RoomCall)?.roomId == room.roomId
}
}
val callState = when { val callState = when {
roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing( roomInfo?.hasRoomCall == true -> RoomCallState.OnGoing(
canJoinCall = canJoinCall, canJoinCall = canJoinCall,
isUserInTheCall = isUserInTheCall, isUserInTheCall = isUserInTheCall,
isUserLocallyInTheCall = isUserLocallyInTheCall,
) )
else -> RoomCallState.StandBy(canStartCall = canJoinCall) else -> RoomCallState.StandBy(canStartCall = canJoinCall)
} }