mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 20:06:51 +03:00
Merge pull request #3421 from vector-im/feature/fga/call_transfer
Feature/fga/call transfer
This commit is contained in:
commit
575ebdc3e8
17 changed files with 267 additions and 112 deletions
|
@ -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()
|
||||
}
|
|
@ -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<String>?
|
||||
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<String>?
|
||||
var opponentVersion: Int
|
||||
|
||||
var capabilities: CallCapabilities?
|
||||
|
||||
var state: CallState
|
||||
|
||||
/**
|
||||
|
@ -91,8 +89,12 @@ 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?)
|
||||
suspend fun transfer(targetUserId: String,
|
||||
targetRoomId: String?,
|
||||
createCallId: String?,
|
||||
awaitCallId: String?)
|
||||
|
||||
fun addListener(listener: StateListener)
|
||||
fun removeListener(listener: StateListener)
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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 targerRoomId: String? = null,
|
||||
@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?
|
||||
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,18 +24,15 @@ 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
|
||||
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
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
|
@ -192,6 +189,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 +201,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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,18 +17,17 @@
|
|||
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
|
||||
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(
|
||||
|
@ -48,32 +47,38 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall {
|
||||
return MxCallImpl(
|
||||
callId = UUID.randomUUID().toString(),
|
||||
callId = CallIdGenerator.generate(),
|
||||
isOutgoing = true,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -36,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
|
||||
|
@ -43,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,
|
||||
|
@ -61,8 +62,16 @@ internal class MxCallImpl(
|
|||
|
||||
override var opponentPartyId: Optional<String>? = 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
|
||||
|
@ -202,7 +211,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 +225,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) }
|
||||
|
|
1
newsfragment/3420.feature
Normal file
1
newsfragment/3420.feature
Normal file
|
@ -0,0 +1 @@
|
|||
VoIP: support attended transfer
|
|
@ -175,7 +175,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), 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,16 +189,27 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), 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.isLocalOnHold || state.isRemoteOnHold) {
|
||||
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
|
||||
configureCallInfo(state)
|
||||
} else if (state.isLocalOnHold || state.isRemoteOnHold) {
|
||||
views.smallIsHeldIcon.isVisible = true
|
||||
views.callVideoGroup.isInvisible = true
|
||||
views.callInfoGroup.isVisible = true
|
||||
|
@ -220,7 +231,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), 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
|
||||
|
@ -235,10 +246,10 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
views.callConnectingProgress.isVisible = true
|
||||
}
|
||||
}
|
||||
is CallState.Terminated -> {
|
||||
is CallState.Terminated -> {
|
||||
finish()
|
||||
}
|
||||
null -> {
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +258,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)
|
||||
views.participantNameText.text = it.getBestName()
|
||||
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)
|
||||
} else {
|
||||
|
@ -322,13 +337,13 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), 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)
|
||||
|
@ -336,7 +351,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
is VectorCallViewEvents.ShowCallTransferScreen -> {
|
||||
navigator.openCallTransfer(this, callArgs.callId)
|
||||
}
|
||||
null -> {
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,4 +34,5 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
|||
object ToggleCamera : VectorCallViewActions()
|
||||
object ToggleHDSD : VectorCallViewActions()
|
||||
object InitiateCallTransfer : VectorCallViewActions()
|
||||
object TransferCall: VectorCallViewActions()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -111,12 +111,21 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
setState {
|
||||
copy(
|
||||
callState = Success(callState),
|
||||
canOpponentBeTransferred = call.capabilities.supportCallTransfer()
|
||||
canOpponentBeTransferred = call.capabilities.supportCallTransfer(),
|
||||
transferee = computeTransfereeState(call)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeTransfereeState(call: MxCall): VectorCallViewState.TransfereeState {
|
||||
val transfereeCall = callManager.getTransfereeForCallId(call.callId) ?: return VectorCallViewState.TransfereeState.NoTransferee
|
||||
val transfereeRoom = session.getRoomSummary(transfereeCall.nativeRoomId)
|
||||
return transfereeRoom?.displayName?.let {
|
||||
VectorCallViewState.TransfereeState.KnownTransferee(it)
|
||||
} ?: VectorCallViewState.TransfereeState.UnknownTransferee
|
||||
}
|
||||
|
||||
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
|
||||
|
||||
override fun onCurrentCallChange(call: WebRtcCall?) {
|
||||
|
@ -166,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) {
|
||||
|
@ -185,7 +194,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(),
|
||||
transferee = computeTransfereeState(webRtcCall.mxCall)
|
||||
)
|
||||
}
|
||||
updateOtherKnownCall(webRtcCall)
|
||||
|
@ -201,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)
|
||||
|
@ -231,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)
|
||||
)
|
||||
|
@ -254,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 -> {
|
||||
|
@ -272,9 +282,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
|
||||
|
|
|
@ -41,15 +41,22 @@ data class VectorCallViewState(
|
|||
val otherKnownCallInfo: CallInfo? = null,
|
||||
val callInfo: CallInfo = CallInfo(callId),
|
||||
val formattedDuration: String = "",
|
||||
val canOpponentBeTransferred: Boolean = false
|
||||
val canOpponentBeTransferred: Boolean = false,
|
||||
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
|
||||
)
|
||||
|
||||
constructor(callArgs: CallArgs): this(
|
||||
constructor(callArgs: CallArgs) : this(
|
||||
callId = callArgs.callId,
|
||||
roomId = callArgs.signalingRoomId,
|
||||
isVideoCall = callArgs.isVideoCall
|
||||
|
|
|
@ -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
|
||||
|
@ -75,7 +78,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
|
||||
}
|
||||
|
@ -83,8 +86,17 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
_viewEvents.post(CallTransferViewEvents.Loading)
|
||||
call?.mxCall?.transfer(action.selectedUserId, null)
|
||||
if (action.consultFirst) {
|
||||
val dmRoomId = directRoomHelper.ensureDMExists(action.selectedUserId)
|
||||
callManager.startOutgoingCall(
|
||||
nativeRoomId = 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,7 +109,16 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
try {
|
||||
_viewEvents.post(CallTransferViewEvents.Loading)
|
||||
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
|
||||
call?.mxCall?.transfer(result.userId, result.roomId)
|
||||
if (action.consultFirst) {
|
||||
callManager.startOutgoingCall(
|
||||
nativeRoomId = 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)
|
||||
|
|
|
@ -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
|
||||
|
@ -85,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<Session?>,
|
||||
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
||||
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<Session?>,
|
||||
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
||||
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
||||
private val onCallEnded: (String) -> Unit
|
||||
) : MxCall.StateListener {
|
||||
|
||||
interface Listener : MxCall.StateListener {
|
||||
fun onCaptureStateChanged() {}
|
||||
|
@ -118,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
|
||||
|
||||
|
@ -271,7 +276,7 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
|
||||
sessionScope?.launch(dispatcher) {
|
||||
when (mode) {
|
||||
VectorCallActivity.INCOMING_ACCEPT -> {
|
||||
VectorCallActivity.INCOMING_ACCEPT -> {
|
||||
internalAcceptIncomingCall()
|
||||
}
|
||||
VectorCallActivity.INCOMING_RINGING -> {
|
||||
|
@ -289,6 +294,40 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Without consultation
|
||||
*/
|
||||
suspend fun transferToUser(targetUserId: String, targetRoomId: String?) {
|
||||
mxCall.transfer(
|
||||
targetUserId = targetUserId,
|
||||
targetRoomId = targetRoomId,
|
||||
createCallId = CallIdGenerator.generate(),
|
||||
awaitCallId = null
|
||||
)
|
||||
endCall(sendEndSignaling = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* With consultation
|
||||
*/
|
||||
suspend fun transferToCall(transferTargetCall: WebRtcCall) {
|
||||
val newCallId = CallIdGenerator.generate()
|
||||
transferTargetCall.mxCall.transfer(
|
||||
targetUserId = mxCall.opponentUserId,
|
||||
targetRoomId = null,
|
||||
createCallId = null,
|
||||
awaitCallId = newCallId
|
||||
)
|
||||
mxCall.transfer(
|
||||
targetUserId = transferTargetCall.mxCall.opponentUserId,
|
||||
targetRoomId = null,
|
||||
createCallId = newCallId,
|
||||
awaitCallId = null
|
||||
)
|
||||
endCall(sendEndSignaling = false)
|
||||
transferTargetCall.endCall(sendEndSignaling = false)
|
||||
}
|
||||
|
||||
fun acceptIncomingCall() {
|
||||
sessionScope?.launch {
|
||||
Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}")
|
||||
|
@ -729,7 +768,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
|
||||
}
|
||||
|
@ -744,9 +783,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 {
|
||||
|
|
|
@ -147,6 +147,11 @@ class WebRtcCallManager @Inject constructor(
|
|||
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]
|
||||
}
|
||||
|
@ -155,6 +160,10 @@ class WebRtcCallManager @Inject constructor(
|
|||
return callsByRoomId[roomId] ?: emptyList()
|
||||
}
|
||||
|
||||
fun getTransfereeForCallId(callId: String): WebRtcCall? {
|
||||
return transferees[callId]
|
||||
}
|
||||
|
||||
fun getCurrentCall(): WebRtcCall? {
|
||||
return currentCall.get()
|
||||
}
|
||||
|
@ -229,34 +238,31 @@ class WebRtcCallManager @Inject constructor(
|
|||
CallService.onCallTerminated(context, callId)
|
||||
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
|
||||
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
|
||||
transferees.remove(callId)
|
||||
if (getCurrentCall()?.callId == callId) {
|
||||
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) {
|
||||
val signalingRoomId = callUserMapper?.getOrCreateVirtualRoomForRoom(nativeRoomId, otherUserId) ?: nativeRoomId
|
||||
suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean, transferee: WebRtcCall? = null) {
|
||||
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")
|
||||
|
@ -274,7 +280,9 @@ class WebRtcCallManager @Inject constructor(
|
|||
val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
|
||||
val webRtcCall = createWebRtcCall(mxCall, nativeRoomId)
|
||||
currentCall.setAndNotify(webRtcCall)
|
||||
|
||||
if (transferee != null) {
|
||||
transferees[webRtcCall.callId] = transferee
|
||||
}
|
||||
CallService.onOutgoingCallRinging(
|
||||
context = context.applicationContext,
|
||||
callId = mxCall.callId)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3237,7 +3237,9 @@
|
|||
<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="call_transfer_unknown_person">Unknown person</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 -->
|
||||
|
|
Loading…
Reference in a new issue