Merge pull request #3421 from vector-im/feature/fga/call_transfer
Feature/fga/call transfer
This commit is contained in:
commit
575ebdc3e8
|
@ -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 callId: String
|
||||||
val isOutgoing: Boolean
|
val isOutgoing: Boolean
|
||||||
val roomId: String
|
val roomId: String
|
||||||
val opponentUserId: String
|
|
||||||
val isVideoCall: Boolean
|
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
|
const val VOIP_PROTO_VERSION = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
val ourPartyId: String
|
|
||||||
var opponentPartyId: Optional<String>?
|
|
||||||
var opponentVersion: Int
|
|
||||||
|
|
||||||
var capabilities: CallCapabilities?
|
|
||||||
|
|
||||||
var state: CallState
|
var state: CallState
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -91,8 +89,12 @@ interface MxCall : MxCallDetail {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a m.call.replaces event to initiate call transfer.
|
* 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 addListener(listener: StateListener)
|
||||||
fun removeListener(listener: StateListener)
|
fun removeListener(listener: StateListener)
|
||||||
|
|
|
@ -56,6 +56,9 @@ data class CallHangupContent(
|
||||||
@Json(name = "user_hangup")
|
@Json(name = "user_hangup")
|
||||||
USER_HANGUP,
|
USER_HANGUP,
|
||||||
|
|
||||||
|
@Json(name = "replaced")
|
||||||
|
REPLACED,
|
||||||
|
|
||||||
@Json(name = "user_media_failed")
|
@Json(name = "user_media_failed")
|
||||||
USER_MEDIA_FAILED,
|
USER_MEDIA_FAILED,
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ data class CallReplacesContent(
|
||||||
* (possibly waiting for user confirmation) and then continues the transfer in this room.
|
* (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.
|
* 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
|
||||||
*/
|
*/
|
||||||
|
@ -77,6 +77,5 @@ data class CallReplacesContent(
|
||||||
* Optional. The avatar URL of the transfer target.
|
* Optional. The avatar URL of the transfer target.
|
||||||
*/
|
*/
|
||||||
@Json(name = "avatar_url") val avatarUrl: String?
|
@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.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
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.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.CallHangupContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
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.CallNegotiateContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
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.CallSelectAnswerContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
|
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.di.UserId
|
||||||
import org.matrix.android.sdk.internal.session.SessionScope
|
import org.matrix.android.sdk.internal.session.SessionScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.math.BigDecimal
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
@ -192,6 +189,9 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
||||||
// Ignore remote echo
|
// Ignore remote echo
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (event.roomId == null || event.senderId == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event.senderId == userId) {
|
if (event.senderId == userId) {
|
||||||
// discard current call, it's answered by another of my session
|
// discard current call, it's answered by another of my session
|
||||||
activeCallHandler.removeCall(call.callId)
|
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}")
|
Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
call.apply {
|
mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities)
|
||||||
opponentPartyId = Optional.from(content.partyId)
|
|
||||||
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
|
|
||||||
capabilities = content.capabilities ?: CallCapabilities()
|
|
||||||
}
|
|
||||||
callListenersDispatcher.onCallAnswerReceived(content)
|
callListenersDispatcher.onCallAnswerReceived(content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,18 +17,17 @@
|
||||||
package org.matrix.android.sdk.internal.session.call
|
package org.matrix.android.sdk.internal.session.call
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
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.call.MxCall
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
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.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.DeviceId
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
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.call.model.MxCallImpl
|
||||||
import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
|
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.LocalEchoEventFactory
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
import java.math.BigDecimal
|
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MxCallFactory @Inject constructor(
|
internal class MxCallFactory @Inject constructor(
|
||||||
|
@ -48,32 +47,38 @@ internal class MxCallFactory @Inject constructor(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
ourPartyId = deviceId ?: "",
|
ourPartyId = deviceId ?: "",
|
||||||
opponentUserId = opponentUserId,
|
|
||||||
isVideoCall = content.isVideo(),
|
isVideoCall = content.isVideo(),
|
||||||
localEchoEventFactory = localEchoEventFactory,
|
localEchoEventFactory = localEchoEventFactory,
|
||||||
eventSenderProcessor = eventSenderProcessor,
|
eventSenderProcessor = eventSenderProcessor,
|
||||||
matrixConfiguration = matrixConfiguration,
|
matrixConfiguration = matrixConfiguration,
|
||||||
getProfileInfoTask = getProfileInfoTask
|
getProfileInfoTask = getProfileInfoTask
|
||||||
).apply {
|
).apply {
|
||||||
opponentPartyId = Optional.from(content.partyId)
|
updateOpponentData(opponentUserId, content, content.capabilities)
|
||||||
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
|
|
||||||
capabilities = content.capabilities ?: CallCapabilities()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall {
|
fun createOutgoingCall(roomId: String, opponentUserId: String, isVideoCall: Boolean): MxCall {
|
||||||
return MxCallImpl(
|
return MxCallImpl(
|
||||||
callId = UUID.randomUUID().toString(),
|
callId = CallIdGenerator.generate(),
|
||||||
isOutgoing = true,
|
isOutgoing = true,
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
userId = userId,
|
userId = userId,
|
||||||
ourPartyId = deviceId ?: "",
|
ourPartyId = deviceId ?: "",
|
||||||
opponentUserId = opponentUserId,
|
|
||||||
isVideoCall = isVideoCall,
|
isVideoCall = isVideoCall,
|
||||||
localEchoEventFactory = localEchoEventFactory,
|
localEchoEventFactory = localEchoEventFactory,
|
||||||
eventSenderProcessor = eventSenderProcessor,
|
eventSenderProcessor = eventSenderProcessor,
|
||||||
matrixConfiguration = matrixConfiguration,
|
matrixConfiguration = matrixConfiguration,
|
||||||
getProfileInfoTask = getProfileInfoTask
|
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
|
package org.matrix.android.sdk.internal.session.call.model
|
||||||
|
|
||||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
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.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
import org.matrix.android.sdk.api.session.events.model.Content
|
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.CallRejectContent
|
||||||
import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent
|
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.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.session.room.model.call.SdpType
|
||||||
import org.matrix.android.sdk.api.util.Optional
|
import org.matrix.android.sdk.api.util.Optional
|
||||||
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
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.LocalEchoEventFactory
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.math.BigDecimal
|
||||||
|
|
||||||
internal class MxCallImpl(
|
internal class MxCallImpl(
|
||||||
override val callId: String,
|
override val callId: String,
|
||||||
override val isOutgoing: Boolean,
|
override val isOutgoing: Boolean,
|
||||||
override val roomId: String,
|
override val roomId: String,
|
||||||
private val userId: String,
|
private val userId: String,
|
||||||
override val opponentUserId: String,
|
|
||||||
override val isVideoCall: Boolean,
|
override val isVideoCall: Boolean,
|
||||||
override val ourPartyId: String,
|
override val ourPartyId: String,
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
|
@ -61,8 +62,16 @@ internal class MxCallImpl(
|
||||||
|
|
||||||
override var opponentPartyId: Optional<String>? = null
|
override var opponentPartyId: Optional<String>? = null
|
||||||
override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION
|
override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION
|
||||||
|
override lateinit var opponentUserId: String
|
||||||
override var capabilities: CallCapabilities? = null
|
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
|
override var state: CallState = CallState.Idle
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
|
@ -202,7 +211,10 @@ internal class MxCallImpl(
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.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 profileInfoParams = GetProfileInfoTask.Params(targetUserId)
|
||||||
val profileInfo = try {
|
val profileInfo = try {
|
||||||
getProfileInfoTask.execute(profileInfoParams)
|
getProfileInfoTask.execute(profileInfoParams)
|
||||||
|
@ -213,15 +225,16 @@ internal class MxCallImpl(
|
||||||
CallReplacesContent(
|
CallReplacesContent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
partyId = ourPartyId,
|
partyId = ourPartyId,
|
||||||
replacementId = UUID.randomUUID().toString(),
|
replacementId = CallIdGenerator.generate(),
|
||||||
version = MxCall.VOIP_PROTO_VERSION.toString(),
|
version = MxCall.VOIP_PROTO_VERSION.toString(),
|
||||||
targetUser = CallReplacesContent.TargetUser(
|
targetUser = CallReplacesContent.TargetUser(
|
||||||
id = targetUserId,
|
id = targetUserId,
|
||||||
displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String,
|
displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String,
|
||||||
avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String
|
avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String
|
||||||
),
|
),
|
||||||
targerRoomId = targetRoomId,
|
targetRoomId = targetRoomId,
|
||||||
createCall = UUID.randomUUID().toString()
|
awaitCall = awaitCallId,
|
||||||
|
createCall = createCallId
|
||||||
)
|
)
|
||||||
.let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) }
|
.let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) }
|
||||||
.also { eventSenderProcessor.postEvent(it) }
|
.also { eventSenderProcessor.postEvent(it) }
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
VoIP: support attended transfer
|
|
@ -198,7 +198,18 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
}
|
}
|
||||||
is CallState.Connected -> {
|
is CallState.Connected -> {
|
||||||
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||||
if (state.isLocalOnHold || state.isRemoteOnHold) {
|
if (state.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.smallIsHeldIcon.isVisible = true
|
||||||
views.callVideoGroup.isInvisible = true
|
views.callVideoGroup.isInvisible = true
|
||||||
views.callInfoGroup.isVisible = true
|
views.callInfoGroup.isVisible = true
|
||||||
|
@ -247,7 +258,11 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
||||||
state.callInfo.otherUserItem?.let {
|
state.callInfo.otherUserItem?.let {
|
||||||
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
|
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
|
||||||
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter)
|
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter)
|
||||||
|
if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) {
|
||||||
views.participantNameText.text = it.getBestName()
|
views.participantNameText.text = it.getBestName()
|
||||||
|
} else {
|
||||||
|
views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName())
|
||||||
|
}
|
||||||
if (blurAvatar) {
|
if (blurAvatar) {
|
||||||
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter)
|
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -34,4 +34,5 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
||||||
object ToggleCamera : VectorCallViewActions()
|
object ToggleCamera : VectorCallViewActions()
|
||||||
object ToggleHDSD : VectorCallViewActions()
|
object ToggleHDSD : VectorCallViewActions()
|
||||||
object InitiateCallTransfer : VectorCallViewActions()
|
object InitiateCallTransfer : VectorCallViewActions()
|
||||||
|
object TransferCall: VectorCallViewActions()
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@ import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
import com.airbnb.mvrx.Success
|
import com.airbnb.mvrx.Success
|
||||||
import com.airbnb.mvrx.ViewModelContext
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.features.call.audio.CallAudioManager
|
import im.vector.app.features.call.audio.CallAudioManager
|
||||||
|
@ -111,12 +111,21 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
callState = Success(callState),
|
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 {
|
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
|
||||||
|
|
||||||
override fun onCurrentCallChange(call: WebRtcCall?) {
|
override fun onCurrentCallChange(call: WebRtcCall?) {
|
||||||
|
@ -185,7 +194,8 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||||
canSwitchCamera = webRtcCall.canSwitchCamera(),
|
canSwitchCamera = webRtcCall.canSwitchCamera(),
|
||||||
formattedDuration = webRtcCall.formattedDuration(),
|
formattedDuration = webRtcCall.formattedDuration(),
|
||||||
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD,
|
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD,
|
||||||
canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer()
|
canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer(),
|
||||||
|
transferee = computeTransfereeState(webRtcCall.mxCall)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
updateOtherKnownCall(webRtcCall)
|
updateOtherKnownCall(webRtcCall)
|
||||||
|
@ -272,9 +282,20 @@ class VectorCallViewModel @AssistedInject constructor(
|
||||||
VectorCallViewEvents.ShowCallTransferScreen
|
VectorCallViewEvents.ShowCallTransferScreen
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
VectorCallViewActions.TransferCall -> {
|
||||||
|
handleCallTransfer()
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleCallTransfer() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val currentCall = call ?: return@launch
|
||||||
|
val transfereeCall = callManager.getTransfereeForCallId(currentCall.callId) ?: return@launch
|
||||||
|
currentCall.transferToCall(transfereeCall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: VectorCallViewState): VectorCallViewModel
|
fun create(initialState: VectorCallViewState): VectorCallViewModel
|
||||||
|
|
|
@ -41,9 +41,16 @@ data class VectorCallViewState(
|
||||||
val otherKnownCallInfo: CallInfo? = null,
|
val otherKnownCallInfo: CallInfo? = null,
|
||||||
val callInfo: CallInfo = CallInfo(callId),
|
val callInfo: CallInfo = CallInfo(callId),
|
||||||
val formattedDuration: String = "",
|
val formattedDuration: String = "",
|
||||||
val canOpponentBeTransferred: Boolean = false
|
val canOpponentBeTransferred: Boolean = false,
|
||||||
|
val transferee: TransfereeState = TransfereeState.NoTransferee
|
||||||
) : MvRxState {
|
) : MvRxState {
|
||||||
|
|
||||||
|
sealed class TransfereeState {
|
||||||
|
object NoTransferee : TransfereeState()
|
||||||
|
data class KnownTransferee(val name: String) : TransfereeState()
|
||||||
|
object UnknownTransferee : TransfereeState()
|
||||||
|
}
|
||||||
|
|
||||||
data class CallInfo(
|
data class CallInfo(
|
||||||
val callId: String,
|
val callId: String,
|
||||||
val otherUserItem: MatrixItem? = null
|
val otherUserItem: MatrixItem? = null
|
||||||
|
|
|
@ -28,13 +28,16 @@ import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.features.call.dialpad.DialPadLookup
|
import im.vector.app.features.call.dialpad.DialPadLookup
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||||
|
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.call.CallState
|
import org.matrix.android.sdk.api.session.call.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
|
|
||||||
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
|
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
|
||||||
private val dialPadLookup: DialPadLookup,
|
private val dialPadLookup: DialPadLookup,
|
||||||
callManager: WebRtcCallManager)
|
private val directRoomHelper: DirectRoomHelper,
|
||||||
|
private val callManager: WebRtcCallManager)
|
||||||
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
|
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
@ -83,8 +86,17 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
||||||
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
|
private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
_viewEvents.post(CallTransferViewEvents.Loading)
|
if (action.consultFirst) {
|
||||||
call?.mxCall?.transfer(action.selectedUserId, null)
|
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)
|
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
|
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
|
||||||
|
@ -97,7 +109,16 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
|
||||||
try {
|
try {
|
||||||
_viewEvents.post(CallTransferViewEvents.Loading)
|
_viewEvents.post(CallTransferViewEvents.Loading)
|
||||||
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
|
val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
|
||||||
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)
|
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
|
_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.orFalse
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.Session
|
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.CallState
|
||||||
import org.matrix.android.sdk.api.session.call.MxCall
|
import org.matrix.android.sdk.api.session.call.MxCall
|
||||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||||
|
@ -85,8 +86,10 @@ private const val AUDIO_TRACK_ID = "ARDAMSa0"
|
||||||
private const val VIDEO_TRACK_ID = "ARDAMSv0"
|
private const val VIDEO_TRACK_ID = "ARDAMSv0"
|
||||||
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
|
private val DEFAULT_AUDIO_CONSTRAINTS = MediaConstraints()
|
||||||
|
|
||||||
class WebRtcCall(val mxCall: MxCall,
|
class WebRtcCall(
|
||||||
// This is where the call is placed from an ui perspective. In case of virtual room, it can differs from the signalingRoomId.
|
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,
|
val nativeRoomId: String,
|
||||||
private val rootEglBase: EglBase?,
|
private val rootEglBase: EglBase?,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
|
@ -94,7 +97,8 @@ class WebRtcCall(val mxCall: MxCall,
|
||||||
private val sessionProvider: Provider<Session?>,
|
private val sessionProvider: Provider<Session?>,
|
||||||
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
||||||
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
||||||
private val onCallEnded: (String) -> Unit) : MxCall.StateListener {
|
private val onCallEnded: (String) -> Unit
|
||||||
|
) : MxCall.StateListener {
|
||||||
|
|
||||||
interface Listener : MxCall.StateListener {
|
interface Listener : MxCall.StateListener {
|
||||||
fun onCaptureStateChanged() {}
|
fun onCaptureStateChanged() {}
|
||||||
|
@ -118,6 +122,7 @@ class WebRtcCall(val mxCall: MxCall,
|
||||||
}
|
}
|
||||||
|
|
||||||
val callId = mxCall.callId
|
val callId = mxCall.callId
|
||||||
|
|
||||||
// room where call signaling is placed. In case of virtual room it can differs from the nativeRoomId.
|
// room where call signaling is placed. In case of virtual room it can differs from the nativeRoomId.
|
||||||
val signalingRoomId = mxCall.roomId
|
val signalingRoomId = mxCall.roomId
|
||||||
|
|
||||||
|
@ -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() {
|
fun acceptIncomingCall() {
|
||||||
sessionScope?.launch {
|
sessionScope?.launch {
|
||||||
Timber.v("## VOIP acceptIncomingCall from state ${mxCall.state}")
|
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) {
|
if (mxCall.state == CallState.Terminated) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -744,9 +783,9 @@ class WebRtcCall(val mxCall: MxCall,
|
||||||
mxCall.state = CallState.Terminated
|
mxCall.state = CallState.Terminated
|
||||||
sessionScope?.launch(dispatcher) {
|
sessionScope?.launch(dispatcher) {
|
||||||
release()
|
release()
|
||||||
}
|
|
||||||
onCallEnded(callId)
|
onCallEnded(callId)
|
||||||
if (originatedByMe) {
|
}
|
||||||
|
if (sendEndSignaling) {
|
||||||
if (wasRinging) {
|
if (wasRinging) {
|
||||||
mxCall.reject()
|
mxCall.reject()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -147,6 +147,11 @@ class WebRtcCallManager @Inject constructor(
|
||||||
private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>()
|
private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>()
|
||||||
private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>()
|
private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>()
|
||||||
|
|
||||||
|
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||||
|
// call with a different party to this one.
|
||||||
|
// callId (target) -> call (transferee)
|
||||||
|
private val transferees = ConcurrentHashMap<String, WebRtcCall>()
|
||||||
|
|
||||||
fun getCallById(callId: String): WebRtcCall? {
|
fun getCallById(callId: String): WebRtcCall? {
|
||||||
return callsByCallId[callId]
|
return callsByCallId[callId]
|
||||||
}
|
}
|
||||||
|
@ -155,6 +160,10 @@ class WebRtcCallManager @Inject constructor(
|
||||||
return callsByRoomId[roomId] ?: emptyList()
|
return callsByRoomId[roomId] ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getTransfereeForCallId(callId: String): WebRtcCall? {
|
||||||
|
return transferees[callId]
|
||||||
|
}
|
||||||
|
|
||||||
fun getCurrentCall(): WebRtcCall? {
|
fun getCurrentCall(): WebRtcCall? {
|
||||||
return currentCall.get()
|
return currentCall.get()
|
||||||
}
|
}
|
||||||
|
@ -229,12 +238,11 @@ class WebRtcCallManager @Inject constructor(
|
||||||
CallService.onCallTerminated(context, callId)
|
CallService.onCallTerminated(context, callId)
|
||||||
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
|
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
|
||||||
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
|
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
|
||||||
|
transferees.remove(callId)
|
||||||
if (getCurrentCall()?.callId == callId) {
|
if (getCurrentCall()?.callId == callId) {
|
||||||
val otherCall = getCalls().lastOrNull()
|
val otherCall = getCalls().lastOrNull()
|
||||||
currentCall.setAndNotify(otherCall)
|
currentCall.setAndNotify(otherCall)
|
||||||
}
|
}
|
||||||
// This must be done in this thread
|
|
||||||
executor.execute {
|
|
||||||
// There is no active calls
|
// There is no active calls
|
||||||
if (getCurrentCall() == null) {
|
if (getCurrentCall() == null) {
|
||||||
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
|
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
|
||||||
|
@ -251,11 +259,9 @@ class WebRtcCallManager @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Timber.v("## VOIP WebRtcPeerConnectionManager close() executor done")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startOutgoingCall(nativeRoomId: String, otherUserId: String, isVideoCall: Boolean) {
|
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")
|
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
|
||||||
if (getCallsByRoomId(nativeRoomId).isNotEmpty()) {
|
if (getCallsByRoomId(nativeRoomId).isNotEmpty()) {
|
||||||
|
@ -274,7 +280,9 @@ class WebRtcCallManager @Inject constructor(
|
||||||
val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
|
val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
|
||||||
val webRtcCall = createWebRtcCall(mxCall, nativeRoomId)
|
val webRtcCall = createWebRtcCall(mxCall, nativeRoomId)
|
||||||
currentCall.setAndNotify(webRtcCall)
|
currentCall.setAndNotify(webRtcCall)
|
||||||
|
if (transferee != null) {
|
||||||
|
transferees[webRtcCall.callId] = transferee
|
||||||
|
}
|
||||||
CallService.onOutgoingCallRinging(
|
CallService.onOutgoingCallRinging(
|
||||||
context = context.applicationContext,
|
context = context.applicationContext,
|
||||||
callId = mxCall.callId)
|
callId = mxCall.callId)
|
||||||
|
|
|
@ -52,7 +52,6 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_centerVertical="true"
|
android:layout_centerVertical="true"
|
||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentStart="true"
|
||||||
android:enabled="false"
|
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
|
@ -3237,7 +3237,9 @@
|
||||||
<string name="call_transfer_title">Transfer</string>
|
<string name="call_transfer_title">Transfer</string>
|
||||||
<string name="call_transfer_failure">An error occurred while transferring call</string>
|
<string name="call_transfer_failure">An error occurred while transferring call</string>
|
||||||
<string name="call_transfer_users_tab_title">Users</string>
|
<string name="call_transfer_users_tab_title">Users</string>
|
||||||
|
<string name="call_transfer_consulting_with">Consulting with %1$s</string>
|
||||||
|
<string name="call_transfer_transfer_to_title">Transfer to %1$s</string>
|
||||||
|
<string name="call_transfer_unknown_person">Unknown person</string>
|
||||||
|
|
||||||
<string name="re_authentication_activity_title">Re-Authentication Needed</string>
|
<string name="re_authentication_activity_title">Re-Authentication Needed</string>
|
||||||
<!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->
|
<!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->
|
||||||
|
|
Loading…
Reference in New Issue