Call transfer: start branching consult first action

This commit is contained in:
ganfra 2021-05-26 12:09:59 +02:00
parent 8eeae51cc6
commit bd8e46c84f
9 changed files with 90 additions and 21 deletions

View File

@ -198,7 +198,14 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
is CallState.Connected -> { is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold || state.isRemoteOnHold) { if(state.transfereeName.hasValue()){
views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, state.transfereeName.get())
views.callActionText.isVisible = true
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) }
views.callStatusText.text = state.formattedDuration
configureCallInfo(state)
}
else if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true views.smallIsHeldIcon.isVisible = true
views.callVideoGroup.isInvisible = true views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
@ -247,7 +254,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
state.callInfo.otherUserItem?.let { state.callInfo.otherUserItem?.let {
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter)
if(state.transfereeName.hasValue()) {
views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName())
}else {
views.participantNameText.text = it.getBestName() views.participantNameText.text = it.getBestName()
}
if (blurAvatar) { if (blurAvatar) {
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter) avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter)
} else { } else {

View File

@ -18,6 +18,7 @@ package im.vector.app.features.call
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.webrtc.WebRtcCall
sealed class VectorCallViewActions : VectorViewModelAction { sealed class VectorCallViewActions : VectorViewModelAction {
object EndCall : VectorCallViewActions() object EndCall : VectorCallViewActions()
@ -34,4 +35,5 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object ToggleCamera : VectorCallViewActions() object ToggleCamera : VectorCallViewActions()
object ToggleHDSD : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions()
object InitiateCallTransfer : VectorCallViewActions() object InitiateCallTransfer : VectorCallViewActions()
object TransferCall: VectorCallViewActions()
} }

View File

@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
class VectorCallViewModel @AssistedInject constructor( class VectorCallViewModel @AssistedInject constructor(
@ -109,15 +110,24 @@ class VectorCallViewModel @AssistedInject constructor(
} }
} }
} }
val transfereeName = computeTransfereeNameIfAny(call)
setState { setState {
copy( copy(
callState = Success(callState), callState = Success(callState),
canOpponentBeTransferred = call.capabilities.supportCallTransfer() canOpponentBeTransferred = call.capabilities.supportCallTransfer(),
transfereeName = transfereeName
) )
} }
} }
} }
private fun computeTransfereeNameIfAny(call: MxCall): Optional<String> {
val transfereeCall = callManager.getTransfereeForCallId(call.callId) ?: return Optional.empty()
val transfereeRoom = session.getRoomSummary(transfereeCall.roomId)
val transfereeName = transfereeRoom?.displayName ?: "Unknown person"
return Optional.from(transfereeName)
}
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
override fun onCurrentCallChange(call: WebRtcCall?) { override fun onCurrentCallChange(call: WebRtcCall?) {
@ -186,7 +196,8 @@ class VectorCallViewModel @AssistedInject constructor(
canSwitchCamera = webRtcCall.canSwitchCamera(), canSwitchCamera = webRtcCall.canSwitchCamera(),
formattedDuration = webRtcCall.formattedDuration(), formattedDuration = webRtcCall.formattedDuration(),
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD, isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD,
canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer() canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer(),
transfereeName = computeTransfereeNameIfAny(webRtcCall.mxCall)
) )
} }
updateOtherKnownCall(webRtcCall) updateOtherKnownCall(webRtcCall)
@ -273,9 +284,20 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewEvents.ShowCallTransferScreen VectorCallViewEvents.ShowCallTransferScreen
) )
} }
VectorCallViewActions.TransferCall -> {
handleCallTransfer()
}
}.exhaustive }.exhaustive
} }
private fun handleCallTransfer() {
viewModelScope.launch {
val currentCall = call ?: return@launch
val transfereeCall = callManager.getTransfereeForCallId(currentCall.callId) ?: return@launch
currentCall.transferToCall(transfereeCall)
}
}
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(initialState: VectorCallViewState): VectorCallViewModel fun create(initialState: VectorCallViewState): VectorCallViewModel

View File

@ -22,6 +22,7 @@ import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.audio.CallAudioManager
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.Optional
data class VectorCallViewState( data class VectorCallViewState(
val callId: String, val callId: String,
@ -41,7 +42,8 @@ data class VectorCallViewState(
val otherKnownCallInfo: CallInfo? = null, val otherKnownCallInfo: CallInfo? = null,
val callInfo: CallInfo = CallInfo(callId), val callInfo: CallInfo = CallInfo(callId),
val formattedDuration: String = "", val formattedDuration: String = "",
val canOpponentBeTransferred: Boolean = false val canOpponentBeTransferred: Boolean = false,
val transfereeName: Optional<String> = Optional.empty()
) : MvRxState { ) : MvRxState {
data class CallInfo( data class CallInfo(

View File

@ -28,13 +28,16 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.dialpad.DialPadLookup import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
private val dialPadLookup: DialPadLookup, private val dialPadLookup: DialPadLookup,
callManager: WebRtcCallManager) private val directRoomHelper: DirectRoomHelper,
private val callManager: WebRtcCallManager)
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) { : VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -83,9 +86,18 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) { private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
viewModelScope.launch { viewModelScope.launch {
try { try {
_viewEvents.post(CallTransferViewEvents.Loading) if (action.consultFirst) {
val dmRoomId = directRoomHelper.ensureDMExists(action.selectedUserId)
callManager.startOutgoingCall(
signalingRoomId = dmRoomId,
otherUserId = action.selectedUserId,
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
transferee = call
)
} else {
call?.transferToUser(action.selectedUserId, null) call?.transferToUser(action.selectedUserId, null)
_viewEvents.post(CallTransferViewEvents.Dismiss) _viewEvents.post(CallTransferViewEvents.Dismiss)
}
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(CallTransferViewEvents.FailToTransfer) _viewEvents.post(CallTransferViewEvents.FailToTransfer)
} }
@ -97,8 +109,17 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
try { try {
_viewEvents.post(CallTransferViewEvents.Loading) _viewEvents.post(CallTransferViewEvents.Loading)
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber) val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
if (action.consultFirst) {
callManager.startOutgoingCall(
signalingRoomId = result.roomId,
otherUserId = result.userId,
isVideoCall = call?.mxCall?.isVideoCall.orFalse(),
transferee = call
)
} else {
call?.transferToUser(result.userId, result.roomId) call?.transferToUser(result.userId, result.roomId)
_viewEvents.post(CallTransferViewEvents.Dismiss) _viewEvents.post(CallTransferViewEvents.Dismiss)
}
} catch (failure: Throwable) { } catch (failure: Throwable) {
_viewEvents.post(CallTransferViewEvents.FailToTransfer) _viewEvents.post(CallTransferViewEvents.FailToTransfer)
} }

View File

@ -287,7 +287,7 @@ class WebRtcCall(val mxCall: MxCall,
} }
} }
suspend fun transferToUser(targetUserId: String, targetRoomId: String?) { suspend fun transferToUser(targetUserId: String, targetRoomId: String?) = withContext(dispatcher){
mxCall.transfer( mxCall.transfer(
targetUserId = targetUserId, targetUserId = targetUserId,
targetRoomId = targetRoomId, targetRoomId = targetRoomId,
@ -297,21 +297,21 @@ class WebRtcCall(val mxCall: MxCall,
endCall(true, CallHangupContent.Reason.REPLACED) endCall(true, CallHangupContent.Reason.REPLACED)
} }
suspend fun transferToCall(transferTargetCall: WebRtcCall) { suspend fun transferToCall(transferTargetCall: WebRtcCall)= withContext(dispatcher) {
val newCallId = CallIdGenerator.generate() val newCallId = CallIdGenerator.generate()
transferTargetCall.mxCall.transfer( transferTargetCall.mxCall.transfer(
targetUserId = this.mxCall.opponentUserId, targetUserId = this@WebRtcCall.mxCall.opponentUserId,
targetRoomId = null, targetRoomId = null,
createCallId = null, createCallId = null,
awaitCallId = newCallId awaitCallId = newCallId
) )
this.mxCall.transfer( this@WebRtcCall.mxCall.transfer(
transferTargetCall.mxCall.opponentUserId, targetUserId = transferTargetCall.mxCall.opponentUserId,
targetRoomId = null, targetRoomId = null,
createCallId = newCallId, createCallId = newCallId,
awaitCallId = null awaitCallId = null
) )
endCall(true, CallHangupContent.Reason.REPLACED) this@WebRtcCall.endCall(true, CallHangupContent.Reason.REPLACED)
transferTargetCall.endCall(true, CallHangupContent.Reason.REPLACED) transferTargetCall.endCall(true, CallHangupContent.Reason.REPLACED)
} }

View File

@ -137,6 +137,10 @@ class WebRtcCallManager @Inject constructor(
private val advertisedCalls = HashSet<String>() private val advertisedCalls = HashSet<String>()
private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>() private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>()
private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>() private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>()
// Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one.
// callId (target) -> call (transferee)
private val transferees = ConcurrentHashMap<String, WebRtcCall>()
fun getCallById(callId: String): WebRtcCall? { fun getCallById(callId: String): WebRtcCall? {
return callsByCallId[callId] return callsByCallId[callId]
@ -146,6 +150,10 @@ class WebRtcCallManager @Inject constructor(
return callsByRoomId[roomId] ?: emptyList() return callsByRoomId[roomId] ?: emptyList()
} }
fun getTransfereeForCallId(callId: String): WebRtcCall? {
return transferees[callId]
}
fun getCurrentCall(): WebRtcCall? { fun getCurrentCall(): WebRtcCall? {
return currentCall.get() return currentCall.get()
} }
@ -219,6 +227,7 @@ class WebRtcCallManager @Inject constructor(
} }
CallService.onCallTerminated(context, callId) CallService.onCallTerminated(context, callId)
callsByRoomId[webRtcCall.roomId]?.remove(webRtcCall) callsByRoomId[webRtcCall.roomId]?.remove(webRtcCall)
transferees.remove(callId)
if (getCurrentCall()?.callId == callId) { if (getCurrentCall()?.callId == callId) {
val otherCall = getCalls().lastOrNull() val otherCall = getCalls().lastOrNull()
currentCall.setAndNotify(otherCall) currentCall.setAndNotify(otherCall)
@ -245,7 +254,7 @@ class WebRtcCallManager @Inject constructor(
} }
} }
fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean, transferee: WebRtcCall? = null) {
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
if (getCallsByRoomId(signalingRoomId).isNotEmpty()) { if (getCallsByRoomId(signalingRoomId).isNotEmpty()) {
Timber.w("## VOIP you already have a call in this room") Timber.w("## VOIP you already have a call in this room")
@ -263,7 +272,9 @@ class WebRtcCallManager @Inject constructor(
val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
val webRtcCall = createWebRtcCall(mxCall) val webRtcCall = createWebRtcCall(mxCall)
currentCall.setAndNotify(webRtcCall) currentCall.setAndNotify(webRtcCall)
if(transferee != null){
transferees[webRtcCall.callId] = transferee
}
CallService.onOutgoingCallRinging( CallService.onOutgoingCallRinging(
context = context.applicationContext, context = context.applicationContext,
callId = mxCall.callId) callId = mxCall.callId)

View File

@ -52,7 +52,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:enabled="false"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView

View File

@ -3231,7 +3231,8 @@
<string name="call_transfer_title">Transfer</string> <string name="call_transfer_title">Transfer</string>
<string name="call_transfer_failure">An error occurred while transferring call</string> <string name="call_transfer_failure">An error occurred while transferring call</string>
<string name="call_transfer_users_tab_title">Users</string> <string name="call_transfer_users_tab_title">Users</string>
<string name="call_transfer_consulting_with">Consulting with %1$s</string>
<string name="call_transfer_transfer_to_title">Transfer to %1$s</string>
<string name="re_authentication_activity_title">Re-Authentication Needed</string> <string name="re_authentication_activity_title">Re-Authentication Needed</string>
<!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name --> <!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->