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 -> {
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<ActivityCallBinding>(), 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)
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 {

View file

@ -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()
}

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.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<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 {
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

View file

@ -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<String> = Optional.empty()
) : MvRxState {
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.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<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(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)
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)
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)
}

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(
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)
}

View file

@ -137,6 +137,10 @@ class WebRtcCallManager @Inject constructor(
private val advertisedCalls = HashSet<String>()
private val callsByCallId = ConcurrentHashMap<String, 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? {
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)

View file

@ -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"/>
<TextView

View file

@ -3231,7 +3231,8 @@
<string name="call_transfer_title">Transfer</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_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>
<!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->