From bd8e46c84f8ea4b3739f990f66143b71963ebe7e Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 26 May 2021 12:09:59 +0200 Subject: [PATCH] Call transfer: start branching consult first action --- .../app/features/call/VectorCallActivity.kt | 15 +++++++-- .../features/call/VectorCallViewActions.kt | 2 ++ .../app/features/call/VectorCallViewModel.kt | 26 +++++++++++++-- .../app/features/call/VectorCallViewState.kt | 4 ++- .../call/transfer/CallTransferViewModel.kt | 33 +++++++++++++++---- .../app/features/call/webrtc/WebRtcCall.kt | 12 +++---- .../features/call/webrtc/WebRtcCallManager.kt | 15 +++++++-- .../res/layout/activity_call_transfer.xml | 1 - vector/src/main/res/values/strings.xml | 3 +- 9 files changed, 90 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index a9e2982714..6e5a0d5ace 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -198,7 +198,14 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } is CallState.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.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -247,7 +254,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro state.callInfo.otherUserItem?.let { val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) - views.participantNameText.text = it.getBestName() + if(state.transfereeName.hasValue()) { + views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) + }else { + views.participantNameText.text = it.getBestName() + } if (blurAvatar) { avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter) } else { diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index 7addabf724..804d272d7f 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -18,6 +18,7 @@ package im.vector.app.features.call import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.call.audio.CallAudioManager +import im.vector.app.features.call.webrtc.WebRtcCall sealed class VectorCallViewActions : VectorViewModelAction { object EndCall : VectorCallViewActions() @@ -34,4 +35,5 @@ sealed class VectorCallViewActions : VectorViewModelAction { object ToggleCamera : VectorCallViewActions() object ToggleHDSD : VectorCallViewActions() object InitiateCallTransfer : VectorCallViewActions() + object TransferCall: VectorCallViewActions() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 8a2d56a5a2..c00326f05e 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -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.room.model.call.supportCallTransfer import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toMatrixItem class VectorCallViewModel @AssistedInject constructor( @@ -109,15 +110,24 @@ class VectorCallViewModel @AssistedInject constructor( } } } + val transfereeName = computeTransfereeNameIfAny(call) setState { copy( callState = Success(callState), - canOpponentBeTransferred = call.capabilities.supportCallTransfer() + canOpponentBeTransferred = call.capabilities.supportCallTransfer(), + transfereeName = transfereeName ) } } } + private fun computeTransfereeNameIfAny(call: MxCall): Optional { + 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 { override fun onCurrentCallChange(call: WebRtcCall?) { @@ -186,7 +196,8 @@ class VectorCallViewModel @AssistedInject constructor( canSwitchCamera = webRtcCall.canSwitchCamera(), formattedDuration = webRtcCall.formattedDuration(), 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) @@ -273,9 +284,20 @@ class VectorCallViewModel @AssistedInject constructor( VectorCallViewEvents.ShowCallTransferScreen ) } + VectorCallViewActions.TransferCall -> { + handleCallTransfer() + } }.exhaustive } + private fun handleCallTransfer() { + viewModelScope.launch { + val currentCall = call ?: return@launch + val transfereeCall = callManager.getTransfereeForCallId(currentCall.callId) ?: return@launch + currentCall.transferToCall(transfereeCall) + } + } + @AssistedFactory interface Factory { fun create(initialState: VectorCallViewState): VectorCallViewModel diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index cdd002114a..4129004d7e 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Uninitialized import im.vector.app.features.call.audio.CallAudioManager import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.Optional data class VectorCallViewState( val callId: String, @@ -41,7 +42,8 @@ data class VectorCallViewState( val otherKnownCallInfo: CallInfo? = null, val callInfo: CallInfo = CallInfo(callId), val formattedDuration: String = "", - val canOpponentBeTransferred: Boolean = false + val canOpponentBeTransferred: Boolean = false, + val transfereeName: Optional = Optional.empty() ) : MvRxState { data class CallInfo( diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt index b2371fcdec..d6745d9592 100644 --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt @@ -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.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager +import im.vector.app.features.createdirect.DirectRoomHelper 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.MxCall class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState, private val dialPadLookup: DialPadLookup, - callManager: WebRtcCallManager) + private val directRoomHelper: DirectRoomHelper, + private val callManager: WebRtcCallManager) : VectorViewModel(initialState) { @AssistedFactory @@ -83,9 +86,18 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) { viewModelScope.launch { try { - _viewEvents.post(CallTransferViewEvents.Loading) - call?.transferToUser(action.selectedUserId, null) - _viewEvents.post(CallTransferViewEvents.Dismiss) + 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) + _viewEvents.post(CallTransferViewEvents.Dismiss) + } } catch (failure: Throwable) { _viewEvents.post(CallTransferViewEvents.FailToTransfer) } @@ -97,8 +109,17 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: try { _viewEvents.post(CallTransferViewEvents.Loading) val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber) - call?.transferToUser(result.userId, result.roomId) - _viewEvents.post(CallTransferViewEvents.Dismiss) + 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) + _viewEvents.post(CallTransferViewEvents.Dismiss) + } } catch (failure: Throwable) { _viewEvents.post(CallTransferViewEvents.FailToTransfer) } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 4aed01965b..81fa41dc0d 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -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( targetUserId = targetUserId, targetRoomId = targetRoomId, @@ -297,21 +297,21 @@ class WebRtcCall(val mxCall: MxCall, endCall(true, CallHangupContent.Reason.REPLACED) } - suspend fun transferToCall(transferTargetCall: WebRtcCall) { + suspend fun transferToCall(transferTargetCall: WebRtcCall)= withContext(dispatcher) { val newCallId = CallIdGenerator.generate() transferTargetCall.mxCall.transfer( - targetUserId = this.mxCall.opponentUserId, + targetUserId = this@WebRtcCall.mxCall.opponentUserId, targetRoomId = null, createCallId = null, awaitCallId = newCallId ) - this.mxCall.transfer( - transferTargetCall.mxCall.opponentUserId, + this@WebRtcCall.mxCall.transfer( + targetUserId = transferTargetCall.mxCall.opponentUserId, targetRoomId = null, createCallId = newCallId, awaitCallId = null ) - endCall(true, CallHangupContent.Reason.REPLACED) + this@WebRtcCall.endCall(true, CallHangupContent.Reason.REPLACED) transferTargetCall.endCall(true, CallHangupContent.Reason.REPLACED) } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 2f8f84051e..0ed91ac821 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -137,6 +137,10 @@ class WebRtcCallManager @Inject constructor( private val advertisedCalls = HashSet() private val callsByCallId = ConcurrentHashMap() private val callsByRoomId = ConcurrentHashMap>() + // 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() fun getCallById(callId: String): WebRtcCall? { return callsByCallId[callId] @@ -146,6 +150,10 @@ class WebRtcCallManager @Inject constructor( return callsByRoomId[roomId] ?: emptyList() } + fun getTransfereeForCallId(callId: String): WebRtcCall? { + return transferees[callId] + } + fun getCurrentCall(): WebRtcCall? { return currentCall.get() } @@ -219,6 +227,7 @@ class WebRtcCallManager @Inject constructor( } CallService.onCallTerminated(context, callId) callsByRoomId[webRtcCall.roomId]?.remove(webRtcCall) + transferees.remove(callId) if (getCurrentCall()?.callId == callId) { val otherCall = getCalls().lastOrNull() 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") if (getCallsByRoomId(signalingRoomId).isNotEmpty()) { 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 webRtcCall = createWebRtcCall(mxCall) currentCall.setAndNotify(webRtcCall) - + if(transferee != null){ + transferees[webRtcCall.callId] = transferee + } CallService.onOutgoingCallRinging( context = context.applicationContext, callId = mxCall.callId) diff --git a/vector/src/main/res/layout/activity_call_transfer.xml b/vector/src/main/res/layout/activity_call_transfer.xml index 64ddd29319..5540eb91d3 100644 --- a/vector/src/main/res/layout/activity_call_transfer.xml +++ b/vector/src/main/res/layout/activity_call_transfer.xml @@ -52,7 +52,6 @@ android:layout_width="wrap_content" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:enabled="false" android:layout_height="wrap_content"/> Transfer An error occurred while transferring call Users - + Consulting with %1$s + Transfer to %1$s Re-Authentication Needed