From 8eeae51cc613fb9e257dc3aed499efa3b246cbfe Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 May 2021 15:21:54 +0200 Subject: [PATCH 1/6] Call transfer: prepare code for consult feature --- .../sdk/api/session/call/CallIdGenerator.kt | 23 ++++++++++++++ .../android/sdk/api/session/call/MxCall.kt | 5 ++- .../room/model/call/CallHangupContent.kt | 3 ++ .../room/model/call/CallReplacesContent.kt | 2 +- .../internal/session/call/MxCallFactory.kt | 3 +- .../internal/session/call/model/MxCallImpl.kt | 13 +++++--- .../call/transfer/CallTransferViewModel.kt | 6 ++-- .../app/features/call/webrtc/WebRtcCall.kt | 31 ++++++++++++++++++- 8 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt new file mode 100644 index 0000000000..43e6872525 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallIdGenerator.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.call + +import java.util.UUID + +object CallIdGenerator { + fun generate() = UUID.randomUUID().toString() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index 7533619eb0..a8a3cf58aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -92,7 +92,10 @@ interface MxCall : MxCallDetail { /** * Send a m.call.replaces event to initiate call transfer. */ - suspend fun transfer(targetUserId: String, targetRoomId: String?) + suspend fun transfer(targetUserId: String, + targetRoomId: String?, + createCallId: String?, + awaitCallId: String?) fun addListener(listener: StateListener) fun removeListener(listener: StateListener) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt index 0acc409053..7edf2dfdc8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -56,6 +56,9 @@ data class CallHangupContent( @Json(name = "user_hangup") USER_HANGUP, + @Json(name = "replaced") + REPLACED, + @Json(name = "user_media_failed") USER_MEDIA_FAILED, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt index 97a3b8c7a7..8746bffda1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -42,7 +42,7 @@ data class CallReplacesContent( * (possibly waiting for user confirmation) and then continues the transfer in this room. * If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing. */ - @Json(name = "target_room") val targerRoomId: String? = null, + @Json(name = "target_room") val targetRoomId: String? = null, /** * An object giving information about the transfer target */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index b14cdca63c..b6aed98504 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.call import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.call.CallIdGenerator import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent @@ -63,7 +64,7 @@ internal class MxCallFactory @Inject constructor( fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall { return MxCallImpl( - callId = UUID.randomUUID().toString(), + callId = CallIdGenerator.generate(), isOutgoing = true, roomId = roomId, userId = userId, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index 88fba0ea85..6db2989a2e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.call.model import org.matrix.android.sdk.api.MatrixConfiguration +import org.matrix.android.sdk.api.session.call.CallIdGenerator 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.events.model.Content @@ -202,7 +203,10 @@ internal class MxCallImpl( .also { eventSenderProcessor.postEvent(it) } } - override suspend fun transfer(targetUserId: String, targetRoomId: String?) { + override suspend fun transfer(targetUserId: String, + targetRoomId: String?, + createCallId: String?, + awaitCallId: String?) { val profileInfoParams = GetProfileInfoTask.Params(targetUserId) val profileInfo = try { getProfileInfoTask.execute(profileInfoParams) @@ -213,15 +217,16 @@ internal class MxCallImpl( CallReplacesContent( callId = callId, partyId = ourPartyId, - replacementId = UUID.randomUUID().toString(), + replacementId = CallIdGenerator.generate(), version = MxCall.VOIP_PROTO_VERSION.toString(), targetUser = CallReplacesContent.TargetUser( id = targetUserId, displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String, avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String ), - targerRoomId = targetRoomId, - createCall = UUID.randomUUID().toString() + targetRoomId = targetRoomId, + awaitCall = awaitCallId, + createCall = createCallId ) .let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } 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 5f661faf80..b2371fcdec 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 @@ -75,7 +75,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: override fun handle(action: CallTransferAction) { when (action) { - is CallTransferAction.ConnectWithUserId -> connectWithUserId(action) + is CallTransferAction.ConnectWithUserId -> connectWithUserId(action) is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action) }.exhaustive } @@ -84,7 +84,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: viewModelScope.launch { try { _viewEvents.post(CallTransferViewEvents.Loading) - call?.mxCall?.transfer(action.selectedUserId, null) + call?.transferToUser(action.selectedUserId, null) _viewEvents.post(CallTransferViewEvents.Dismiss) } catch (failure: Throwable) { _viewEvents.post(CallTransferViewEvents.FailToTransfer) @@ -97,7 +97,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: try { _viewEvents.post(CallTransferViewEvents.Loading) val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber) - call?.mxCall?.transfer(result.userId, result.roomId) + 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 a3a1a29c4b..4aed01965b 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 @@ -45,6 +45,7 @@ import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.call.CallIdGenerator 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.MxPeerConnectionState @@ -268,7 +269,7 @@ class WebRtcCall(val mxCall: MxCall, sessionScope?.launch(dispatcher) { when (mode) { - VectorCallActivity.INCOMING_ACCEPT -> { + VectorCallActivity.INCOMING_ACCEPT -> { internalAcceptIncomingCall() } VectorCallActivity.INCOMING_RINGING -> { @@ -286,6 +287,34 @@ class WebRtcCall(val mxCall: MxCall, } } + suspend fun transferToUser(targetUserId: String, targetRoomId: String?) { + mxCall.transfer( + targetUserId = targetUserId, + targetRoomId = targetRoomId, + createCallId = CallIdGenerator.generate(), + awaitCallId = null + ) + endCall(true, CallHangupContent.Reason.REPLACED) + } + + suspend fun transferToCall(transferTargetCall: WebRtcCall) { + val newCallId = CallIdGenerator.generate() + transferTargetCall.mxCall.transfer( + targetUserId = this.mxCall.opponentUserId, + targetRoomId = null, + createCallId = null, + awaitCallId = newCallId + ) + this.mxCall.transfer( + transferTargetCall.mxCall.opponentUserId, + targetRoomId = null, + createCallId = newCallId, + awaitCallId = null + ) + endCall(true, CallHangupContent.Reason.REPLACED) + transferTargetCall.endCall(true, CallHangupContent.Reason.REPLACED) + } + fun acceptIncomingCall() { sessionScope?.launch { Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}") From bd8e46c84f8ea4b3739f990f66143b71963ebe7e Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 26 May 2021 12:09:59 +0200 Subject: [PATCH 2/6] 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 From bcc360692ee236a7e5eb447461f2f2875a0f6db7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 27 May 2021 16:00:32 +0200 Subject: [PATCH 3/6] Call transfer: makes call transfer working properly --- .../android/sdk/api/session/call/MxCall.kt | 12 +++---- .../session/call/CallSignalingHandler.kt | 11 +++--- .../internal/session/call/MxCallFactory.kt | 19 +++++----- .../internal/session/call/model/MxCallImpl.kt | 12 +++++-- .../call/transfer/CallTransferViewModel.kt | 4 +-- .../app/features/call/webrtc/WebRtcCall.kt | 16 ++++----- .../features/call/webrtc/WebRtcCallManager.kt | 35 +++++++++---------- 7 files changed, 55 insertions(+), 54 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index a8a3cf58aa..08278d8e4f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -26,8 +26,12 @@ interface MxCallDetail { val callId: String val isOutgoing: Boolean val roomId: String - val opponentUserId: String val isVideoCall: Boolean + val ourPartyId: String + val opponentPartyId: Optional? + val opponentVersion: Int + val opponentUserId: String + val capabilities: CallCapabilities? } /** @@ -39,12 +43,6 @@ interface MxCall : MxCallDetail { const val VOIP_PROTO_VERSION = 1 } - val ourPartyId: String - var opponentPartyId: Optional? - var opponentVersion: Int - - var capabilities: CallCapabilities? - var state: CallState /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 6bf11ab78f..dbf15d2624 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -24,7 +24,6 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent -import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent @@ -35,7 +34,6 @@ import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @SessionScope @@ -192,6 +190,9 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa // Ignore remote echo return } + if (event.roomId == null || event.senderId == null) { + return + } if (event.senderId == userId) { // discard current call, it's answered by another of my session activeCallHandler.removeCall(call.callId) @@ -201,11 +202,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}") return } - call.apply { - opponentPartyId = Optional.from(content.partyId) - opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION - capabilities = content.capabilities ?: CallCapabilities() - } + mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities) callListenersDispatcher.onCallAnswerReceived(content) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index b6aed98504..68ac4369b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -21,15 +21,13 @@ import org.matrix.android.sdk.api.session.call.CallIdGenerator import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent -import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.call.model.MxCallImpl import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor -import java.math.BigDecimal -import java.util.UUID import javax.inject.Inject internal class MxCallFactory @Inject constructor( @@ -49,16 +47,13 @@ internal class MxCallFactory @Inject constructor( roomId = roomId, userId = userId, ourPartyId = deviceId ?: "", - opponentUserId = opponentUserId, isVideoCall = content.isVideo(), localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, matrixConfiguration = matrixConfiguration, getProfileInfoTask = getProfileInfoTask ).apply { - opponentPartyId = Optional.from(content.partyId) - opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION - capabilities = content.capabilities ?: CallCapabilities() + updateOpponentData(opponentUserId, content, content.capabilities) } } @@ -69,12 +64,18 @@ internal class MxCallFactory @Inject constructor( roomId = roomId, userId = userId, ourPartyId = deviceId ?: "", - opponentUserId = opponentUserId, isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor, matrixConfiguration = matrixConfiguration, getProfileInfoTask = getProfileInfoTask - ) + ).apply { + // Setup with this userId, might be updated when processing the Answer event + this.opponentUserId = opponentUserId + } + } + + fun updateOutgoingCallWithOpponentData(call: MxCall, userId: String, content: CallSignalingContent, callCapabilities: CallCapabilities?) { + (call as? MxCallImpl)?.updateOpponentData(userId, content, callCapabilities) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt index 6db2989a2e..f101685a4b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt @@ -37,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent import org.matrix.android.sdk.api.session.room.model.call.SdpType import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService @@ -44,14 +45,13 @@ import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import timber.log.Timber -import java.util.UUID +import java.math.BigDecimal internal class MxCallImpl( override val callId: String, override val isOutgoing: Boolean, override val roomId: String, private val userId: String, - override val opponentUserId: String, override val isVideoCall: Boolean, override val ourPartyId: String, private val localEchoEventFactory: LocalEchoEventFactory, @@ -62,8 +62,16 @@ internal class MxCallImpl( override var opponentPartyId: Optional? = null override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION + override lateinit var opponentUserId: String override var capabilities: CallCapabilities? = null + fun updateOpponentData(userId: String, content: CallSignalingContent, callCapabilities: CallCapabilities?) { + opponentPartyId = Optional.from(content.partyId) + opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + opponentUserId = userId + capabilities = callCapabilities ?: CallCapabilities() + } + override var state: CallState = CallState.Idle set(value) { field = value 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 8a6302a5a6..0f37ccaa29 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 @@ -96,8 +96,8 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: ) } else { call?.transferToUser(action.selectedUserId, null) - _viewEvents.post(CallTransferViewEvents.Dismiss) } + _viewEvents.post(CallTransferViewEvents.Dismiss) } catch (failure: Throwable) { _viewEvents.post(CallTransferViewEvents.FailToTransfer) } @@ -118,8 +118,8 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: ) } else { call?.transferToUser(result.userId, result.roomId) - _viewEvents.post(CallTransferViewEvents.Dismiss) } + _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 eb382fe907..7abb077ee0 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 @@ -290,17 +290,17 @@ class WebRtcCall(val mxCall: MxCall, } } - suspend fun transferToUser(targetUserId: String, targetRoomId: String?) = withContext(dispatcher){ + suspend fun transferToUser(targetUserId: String, targetRoomId: String?) { mxCall.transfer( targetUserId = targetUserId, targetRoomId = targetRoomId, createCallId = CallIdGenerator.generate(), awaitCallId = null ) - endCall(true, CallHangupContent.Reason.REPLACED) + endCall(sendEndSignaling = false) } - suspend fun transferToCall(transferTargetCall: WebRtcCall)= withContext(dispatcher) { + suspend fun transferToCall(transferTargetCall: WebRtcCall) { val newCallId = CallIdGenerator.generate() transferTargetCall.mxCall.transfer( targetUserId = this@WebRtcCall.mxCall.opponentUserId, @@ -314,8 +314,8 @@ class WebRtcCall(val mxCall: MxCall, createCallId = newCallId, awaitCallId = null ) - this@WebRtcCall.endCall(true, CallHangupContent.Reason.REPLACED) - transferTargetCall.endCall(true, CallHangupContent.Reason.REPLACED) + this@WebRtcCall.endCall(sendEndSignaling = false) + transferTargetCall.endCall(sendEndSignaling = false) } fun acceptIncomingCall() { @@ -758,7 +758,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { + fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) { if (mxCall.state == CallState.Terminated) { return } @@ -773,9 +773,9 @@ class WebRtcCall(val mxCall: MxCall, mxCall.state = CallState.Terminated sessionScope?.launch(dispatcher) { release() + onCallEnded(callId) } - onCallEnded(callId) - if (originatedByMe) { + if (sendEndSignaling) { if (wasRinging) { mxCall.reject() } else { 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 dabe09fc56..3c18d97937 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 @@ -146,6 +146,7 @@ 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) @@ -242,30 +243,26 @@ class WebRtcCallManager @Inject constructor( val otherCall = getCalls().lastOrNull() currentCall.setAndNotify(otherCall) } - // This must be done in this thread - executor.execute { - // There is no active calls - if (getCurrentCall() == null) { - Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") - peerConnectionFactory?.dispose() - peerConnectionFactory = null - audioManager.setMode(CallAudioManager.Mode.DEFAULT) - // did we start background sync? so we should stop it - if (isInBackground) { - if (FcmHelper.isPushSupported()) { - currentSession?.stopAnyBackgroundSync() - } else { - // for fdroid we should not stop, it should continue syncing - // maybe we should restore default timeout/delay though? - } + // There is no active calls + if (getCurrentCall() == null) { + Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") + peerConnectionFactory?.dispose() + peerConnectionFactory = null + audioManager.setMode(CallAudioManager.Mode.DEFAULT) + // did we start background sync? so we should stop it + if (isInBackground) { + if (FcmHelper.isPushSupported()) { + currentSession?.stopAnyBackgroundSync() + } else { + // for fdroid we should not stop, it should continue syncing + // maybe we should restore default timeout/delay though? } } - Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done") } } suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean, transferee: WebRtcCall? = null) { - val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId + val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") if (getCallsByRoomId(nativeRoomId).isNotEmpty()) { Timber.w("## VOIP you already have a call in this room") @@ -283,7 +280,7 @@ class WebRtcCallManager @Inject constructor( val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return val webRtcCall = createWebRtcCall(mxCall, nativeRoomId) currentCall.setAndNotify(webRtcCall) - if(transferee != null){ + if (transferee != null) { transferees[webRtcCall.callId] = transferee } CallService.onOutgoingCallRinging( From 8e8bc0055d58aff8880c204c2a0ea2219c3665d5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 27 May 2021 16:32:14 +0200 Subject: [PATCH 4/6] Call transfer: clean & add changelog --- .../sdk/internal/session/call/CallSignalingHandler.kt | 1 - newsfragment/3420.feature | 1 + .../im/vector/app/features/call/VectorCallActivity.kt | 9 ++++----- .../im/vector/app/features/call/VectorCallViewActions.kt | 1 - .../im/vector/app/features/call/VectorCallViewModel.kt | 2 -- 5 files changed, 5 insertions(+), 9 deletions(-) create mode 100644 newsfragment/3420.feature diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index dbf15d2624..61ea660b60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -30,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent -import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope import timber.log.Timber diff --git a/newsfragment/3420.feature b/newsfragment/3420.feature new file mode 100644 index 0000000000..3f3df52f62 --- /dev/null +++ b/newsfragment/3420.feature @@ -0,0 +1 @@ +VoIP: support attended transfer \ No newline at end of file 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 0233fdf3e8..d8f183a94a 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,14 +198,13 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { - if(state.transfereeName.hasValue()){ + 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) { + } else if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -254,9 +253,9 @@ 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) - if(state.transfereeName.hasValue()) { + if (state.transfereeName.hasValue()) { views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) - }else { + } else { views.participantNameText.text = it.getBestName() } if (blurAvatar) { 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 804d272d7f..a332153aaa 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,7 +18,6 @@ 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() 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 ddb9629d8f..aed38f9e98 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,9 +39,7 @@ 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.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( @Assisted initialState: VectorCallViewState, From 34b012732e42876f3178361e2f895ecfb4feed01 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 28 May 2021 14:28:32 +0200 Subject: [PATCH 5/6] Call transfer: handle unknown person correctly --- .../app/features/call/VectorCallActivity.kt | 35 ++++++++------- .../app/features/call/VectorCallViewModel.kt | 43 +++++++++---------- .../app/features/call/VectorCallViewState.kt | 9 +++- vector/src/main/res/values/strings.xml | 1 + 4 files changed, 49 insertions(+), 39 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 d8f183a94a..ad04e33414 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 @@ -175,7 +175,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro when (callState) { is CallState.Idle, is CallState.CreateOffer, - is CallState.Dialing -> { + is CallState.Dialing -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true views.callStatusText.setText(R.string.call_ring) @@ -189,17 +189,22 @@ class VectorCallActivity : VectorBaseActivity(), CallContro configureCallInfo(state) } - is CallState.Answering -> { + is CallState.Answering -> { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true views.callStatusText.setText(R.string.call_connecting) views.callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { + is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { - if (state.transfereeName.hasValue()) { - views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, state.transfereeName.get()) + if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { + val transfereeName = if (state.transferee is VectorCallViewState.TransfereeState.KnownTransferee) { + state.transferee.name + } else { + getString(R.string.call_transfer_unknown_person) + } + views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName) views.callActionText.isVisible = true views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) } views.callStatusText.text = state.formattedDuration @@ -226,7 +231,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro if (callArgs.isVideoCall) { views.callVideoGroup.isVisible = true views.callInfoGroup.isVisible = false - views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null + views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null } else { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -241,10 +246,10 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callConnectingProgress.isVisible = true } } - is CallState.Terminated -> { + is CallState.Terminated -> { finish() } - null -> { + null -> { } } } @@ -253,10 +258,10 @@ 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) - if (state.transfereeName.hasValue()) { - views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) - } else { + if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { views.participantNameText.text = it.getBestName() + } else { + views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) } if (blurAvatar) { avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter) @@ -332,13 +337,13 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun handleViewEvents(event: VectorCallViewEvents?) { Timber.v("## VOIP handleViewEvents $event") when (event) { - VectorCallViewEvents.DismissNoCall -> { + VectorCallViewEvents.DismissNoCall -> { finish() } - is VectorCallViewEvents.ConnectionTimeout -> { + is VectorCallViewEvents.ConnectionTimeout -> { onErrorTimoutConnect(event.turn) } - is VectorCallViewEvents.ShowDialPad -> { + is VectorCallViewEvents.ShowDialPad -> { CallDialPadBottomSheet.newInstance(false).apply { callback = dialPadCallback }.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG) @@ -346,7 +351,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro is VectorCallViewEvents.ShowCallTransferScreen -> { navigator.openCallTransfer(this, callArgs.callId) } - null -> { + null -> { } } } 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 aed38f9e98..18eda0fd6f 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 @@ -23,8 +23,8 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.ViewModelContext import dagger.assisted.Assisted -import dagger.assisted.AssistedInject import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.call.audio.CallAudioManager @@ -39,7 +39,6 @@ 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.MxPeerConnectionState import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer -import org.matrix.android.sdk.api.util.Optional class VectorCallViewModel @AssistedInject constructor( @Assisted initialState: VectorCallViewState, @@ -109,22 +108,22 @@ class VectorCallViewModel @AssistedInject constructor( } } } - val transfereeName = computeTransfereeNameIfAny(call) setState { copy( callState = Success(callState), canOpponentBeTransferred = call.capabilities.supportCallTransfer(), - transfereeName = transfereeName + transferee = computeTransfereeState(call) ) } } } - private fun computeTransfereeNameIfAny(call: MxCall): Optional { - val transfereeCall = callManager.getTransfereeForCallId(call.callId) ?: return Optional.empty() + private fun computeTransfereeState(call: MxCall): VectorCallViewState.TransfereeState { + val transfereeCall = callManager.getTransfereeForCallId(call.callId) ?: return VectorCallViewState.TransfereeState.NoTransferee val transfereeRoom = session.getRoomSummary(transfereeCall.nativeRoomId) - val transfereeName = transfereeRoom?.displayName ?: "Unknown person" - return Optional.from(transfereeName) + return transfereeRoom?.displayName?.let { + VectorCallViewState.TransfereeState.KnownTransferee(it) + } ?: VectorCallViewState.TransfereeState.UnknownTransferee } private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { @@ -176,7 +175,7 @@ class VectorCallViewModel @AssistedInject constructor( } else { call = webRtcCall callManager.addCurrentCallListener(currentCallListener) - val item = webRtcCall.getOpponentAsMatrixItem(session) + val item = webRtcCall.getOpponentAsMatrixItem(session) webRtcCall.addListener(callListener) val currentSoundDevice = callManager.audioManager.selectedDevice if (currentSoundDevice == CallAudioManager.Device.PHONE) { @@ -196,7 +195,7 @@ class VectorCallViewModel @AssistedInject constructor( formattedDuration = webRtcCall.formattedDuration(), isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD, canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer(), - transfereeName = computeTransfereeNameIfAny(webRtcCall.mxCall) + transferee = computeTransfereeState(webRtcCall.mxCall) ) } updateOtherKnownCall(webRtcCall) @@ -212,27 +211,27 @@ class VectorCallViewModel @AssistedInject constructor( override fun handle(action: VectorCallViewActions) = withState { state -> when (action) { - VectorCallViewActions.EndCall -> call?.endCall() - VectorCallViewActions.AcceptCall -> { + VectorCallViewActions.EndCall -> call?.endCall() + VectorCallViewActions.AcceptCall -> { setState { copy(callState = Loading()) } call?.acceptIncomingCall() } - VectorCallViewActions.DeclineCall -> { + VectorCallViewActions.DeclineCall -> { setState { copy(callState = Loading()) } call?.endCall() } - VectorCallViewActions.ToggleMute -> { + VectorCallViewActions.ToggleMute -> { val muted = state.isAudioMuted call?.muteCall(!muted) setState { copy(isAudioMuted = !muted) } } - VectorCallViewActions.ToggleVideo -> { + VectorCallViewActions.ToggleVideo -> { if (state.isVideoCall) { val videoEnabled = state.isVideoEnabled call?.enableVideo(!videoEnabled) @@ -242,14 +241,14 @@ class VectorCallViewModel @AssistedInject constructor( } Unit } - VectorCallViewActions.ToggleHoldResume -> { + VectorCallViewActions.ToggleHoldResume -> { val isRemoteOnHold = state.isRemoteOnHold call?.updateRemoteOnHold(!isRemoteOnHold) } is VectorCallViewActions.ChangeAudioDevice -> { callManager.audioManager.setAudioDevice(action.device) } - VectorCallViewActions.SwitchSoundDevice -> { + VectorCallViewActions.SwitchSoundDevice -> { _viewEvents.post( VectorCallViewEvents.ShowSoundDeviceChooser(state.availableDevices, state.device) ) @@ -265,17 +264,17 @@ class VectorCallViewModel @AssistedInject constructor( } Unit } - VectorCallViewActions.ToggleCamera -> { + VectorCallViewActions.ToggleCamera -> { call?.switchCamera() } - VectorCallViewActions.ToggleHDSD -> { + VectorCallViewActions.ToggleHDSD -> { if (!state.isVideoCall) return@withState call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD) } - VectorCallViewActions.OpenDialPad -> { + VectorCallViewActions.OpenDialPad -> { _viewEvents.post(VectorCallViewEvents.ShowDialPad) } - is VectorCallViewActions.SendDtmfDigit -> { + is VectorCallViewActions.SendDtmfDigit -> { call?.sendDtmfDigit(action.digit) } VectorCallViewActions.InitiateCallTransfer -> { @@ -283,7 +282,7 @@ class VectorCallViewModel @AssistedInject constructor( VectorCallViewEvents.ShowCallTransferScreen ) } - VectorCallViewActions.TransferCall -> { + VectorCallViewActions.TransferCall -> { handleCallTransfer() } }.exhaustive 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 70f28dcc23..448bda08c4 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,7 +22,6 @@ 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, @@ -43,9 +42,15 @@ data class VectorCallViewState( val callInfo: CallInfo = CallInfo(callId), val formattedDuration: String = "", val canOpponentBeTransferred: Boolean = false, - val transfereeName: Optional = Optional.empty() + val transferee: TransfereeState = TransfereeState.NoTransferee ) : MvRxState { + sealed class TransfereeState { + object NoTransferee: TransfereeState() + data class KnownTransferee(val name:String): TransfereeState() + object UnknownTransferee: TransfereeState() + } + data class CallInfo( val callId: String, val otherUserItem: MatrixItem? = null diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index b1f1c12662..3f49994fd6 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3234,6 +3234,7 @@ Users Consulting with %1$s Transfer to %1$s + Unknown person Re-Authentication Needed From fca74e9eb45127e9516e3daa76f636928fb2e4c8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 28 May 2021 16:36:03 +0200 Subject: [PATCH 6/6] Small cleanup during review --- .../android/sdk/api/session/call/MxCall.kt | 1 + .../room/model/call/CallReplacesContent.kt | 17 ++++----- .../internal/session/call/MxCallFactory.kt | 5 ++- .../app/features/call/VectorCallViewState.kt | 8 ++-- .../app/features/call/webrtc/WebRtcCall.kt | 38 ++++++++++++------- 5 files changed, 41 insertions(+), 28 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt index 08278d8e4f..fcc9f7072d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt @@ -89,6 +89,7 @@ interface MxCall : MxCallDetail { /** * Send a m.call.replaces event to initiate call transfer. + * See [org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent] for documentation about the parameters */ suspend fun transfer(targetUserId: String, targetRoomId: String?, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt index 1ae1c09a35..4559c5db6d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -38,23 +38,23 @@ data class CallReplacesContent( */ @Json(name = "replacement_id") val replacementId: String? = null, /** - * Optional. If specified, the transferee client waits for an invite to this room and joins it - * (possibly waiting for user confirmation) and then continues the transfer in this room. - * If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing. + * Optional. If specified, the transferee client waits for an invite to this room and joins it + * (possibly waiting for user confirmation) and then continues the transfer in this room. + * If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing. */ @Json(name = "target_room") val targetRoomId: String? = null, /** - * An object giving information about the transfer target + * An object giving information about the transfer target */ @Json(name = "target_user") val targetUser: TargetUser? = null, /** - * If specified, gives the call ID for the transferee's client to use when placing the replacement call. - * Mutually exclusive with await_call + * If specified, gives the call ID for the transferee's client to use when placing the replacement call. + * Mutually exclusive with await_call */ @Json(name = "create_call") val createCall: String? = null, /** - * If specified, gives the call ID that the transferee's client should wait for. - * Mutually exclusive with create_call. + * If specified, gives the call ID that the transferee's client should wait for. + * Mutually exclusive with create_call. */ @Json(name = "await_call") val awaitCall: String? = null, /** @@ -77,6 +77,5 @@ data class CallReplacesContent( * Optional. The avatar URL of the transfer target. */ @Json(name = "avatar_url") val avatarUrl: String? - ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt index 68ac4369b3..547be2253f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/MxCallFactory.kt @@ -75,7 +75,10 @@ internal class MxCallFactory @Inject constructor( } } - fun updateOutgoingCallWithOpponentData(call: MxCall, userId: String, content: CallSignalingContent, callCapabilities: CallCapabilities?) { + fun updateOutgoingCallWithOpponentData(call: MxCall, + userId: String, + content: CallSignalingContent, + callCapabilities: CallCapabilities?) { (call as? MxCallImpl)?.updateOpponentData(userId, content, callCapabilities) } } 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 448bda08c4..c5ae61cf60 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 @@ -46,9 +46,9 @@ data class VectorCallViewState( ) : MvRxState { sealed class TransfereeState { - object NoTransferee: TransfereeState() - data class KnownTransferee(val name:String): TransfereeState() - object UnknownTransferee: TransfereeState() + object NoTransferee : TransfereeState() + data class KnownTransferee(val name: String) : TransfereeState() + object UnknownTransferee : TransfereeState() } data class CallInfo( @@ -56,7 +56,7 @@ data class VectorCallViewState( val otherUserItem: MatrixItem? = null ) - constructor(callArgs: CallArgs): this( + constructor(callArgs: CallArgs) : this( callId = callArgs.callId, roomId = callArgs.signalingRoomId, isVideoCall = callArgs.isVideoCall 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 7abb077ee0..f2a008feb7 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 @@ -86,16 +86,19 @@ private const val AUDIO_TRACK_ID = "ARDAMSa0" private const val VIDEO_TRACK_ID = "ARDAMSv0" private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints() -class WebRtcCall(val mxCall: MxCall, - // This is where the call is placed from an ui perspective. In case of virtual room, it can differs from the signalingRoomId. - val nativeRoomId: String, - private val rootEglBase: EglBase?, - private val context: Context, - private val dispatcher: CoroutineContext, - private val sessionProvider: Provider, - private val peerConnectionFactoryProvider: Provider, - private val onCallBecomeActive: (WebRtcCall) -> Unit, - private val onCallEnded: (String) -> Unit) : MxCall.StateListener { +class WebRtcCall( + val mxCall: MxCall, + // This is where the call is placed from an ui perspective. + // In case of virtual room, it can differs from the signalingRoomId. + val nativeRoomId: String, + private val rootEglBase: EglBase?, + private val context: Context, + private val dispatcher: CoroutineContext, + private val sessionProvider: Provider, + private val peerConnectionFactoryProvider: Provider, + private val onCallBecomeActive: (WebRtcCall) -> Unit, + private val onCallEnded: (String) -> Unit +) : MxCall.StateListener { interface Listener : MxCall.StateListener { fun onCaptureStateChanged() {} @@ -119,6 +122,7 @@ class WebRtcCall(val mxCall: MxCall, } val callId = mxCall.callId + // room where call signaling is placed. In case of virtual room it can differs from the nativeRoomId. val signalingRoomId = mxCall.roomId @@ -290,6 +294,9 @@ class WebRtcCall(val mxCall: MxCall, } } + /** + * Without consultation + */ suspend fun transferToUser(targetUserId: String, targetRoomId: String?) { mxCall.transfer( targetUserId = targetUserId, @@ -300,22 +307,25 @@ class WebRtcCall(val mxCall: MxCall, endCall(sendEndSignaling = false) } + /** + * With consultation + */ suspend fun transferToCall(transferTargetCall: WebRtcCall) { val newCallId = CallIdGenerator.generate() transferTargetCall.mxCall.transfer( - targetUserId = this@WebRtcCall.mxCall.opponentUserId, + targetUserId = mxCall.opponentUserId, targetRoomId = null, createCallId = null, awaitCallId = newCallId ) - this@WebRtcCall.mxCall.transfer( + mxCall.transfer( targetUserId = transferTargetCall.mxCall.opponentUserId, targetRoomId = null, createCallId = newCallId, awaitCallId = null ) - this@WebRtcCall.endCall(sendEndSignaling = false) - transferTargetCall.endCall(sendEndSignaling = false) + endCall(sendEndSignaling = false) + transferTargetCall.endCall(sendEndSignaling = false) } fun acceptIncomingCall() {