From 9c5fe81792acd85673cbaa069bdbb49a1bf22a60 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 17 Dec 2020 14:33:22 +0100 Subject: [PATCH] VoIP: start to handle call transfer in SDK --- .../android/sdk/api/MatrixConfiguration.kt | 6 +- .../android/sdk/api/session/call/MxCall.kt | 3 + .../sdk/api/session/events/model/EventType.kt | 1 + .../room/model/call/CallAnswerContent.kt | 6 +- .../room/model/call/CallCapabilities.kt | 32 ++++++++ .../room/model/call/CallInviteContent.kt | 6 +- .../room/model/call/CallReplacesContent.kt | 81 +++++++++++++++++++ .../session/call/CallSignalingHandler.kt | 2 + .../internal/session/call/MxCallFactory.kt | 10 ++- .../internal/session/call/model/MxCallImpl.kt | 22 ++++- 10 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt index 725fd08d3b..5aca07e8cc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt @@ -35,7 +35,11 @@ data class MatrixConfiguration( * Optional proxy to connect to the matrix servers * You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port) */ - val proxy: Proxy? = null + val proxy: Proxy? = null, + /** + * True to advertise support for call transfers to other parties on Matrix calls. + */ + val supportsCallTransfer: Boolean = true ) { /** 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 75cff0e709..97bf724107 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 @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.call import org.matrix.android.sdk.api.session.room.model.call.CallCandidate +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.SdpType import org.matrix.android.sdk.api.util.Optional @@ -42,6 +43,8 @@ interface MxCall : MxCallDetail { 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/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 85ba3100b0..3a4b4f82c3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -72,6 +72,7 @@ object EventType { const val CALL_NEGOTIATE = "m.call.negotiate" const val CALL_REJECT = "m.call.reject" const val CALL_HANGUP = "m.call.hangup" + const val CALL_REPLACES = "m.call.replaces" // Key share events const val ROOM_KEY_REQUEST = "m.room_key_request" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt index 1fe4c3576f..e37595a129 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAnswerContent.kt @@ -39,7 +39,11 @@ data class CallAnswerContent( /** * Required. The version of the VoIP specification this messages adheres to. */ - @Json(name = "version") override val version: String? + @Json(name = "version") override val version: String?, + /** + * Capability advertisement. + */ + @Json(name= "capabilities") val capabilities: CallCapabilities? = null ): CallSignallingContent { @JsonClass(generateAdapter = true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt new file mode 100644 index 0000000000..eb7edbeed3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCapabilities.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.orFalse + +@JsonClass(generateAdapter = true) +data class CallCapabilities( + /** + * If set to true, states that the sender of the event supports the m.call.replaces event and therefore supports + * being transferred to another destination + */ + @Json(name = "m.call.transferee") val transferee: Boolean? = null +) + +fun CallCapabilities?.supportCallTransfer() = this?.transferee.orFalse() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt index dfbc5c64be..c89549184a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallInviteContent.kt @@ -49,7 +49,11 @@ data class CallInviteContent( /** * The field should be added for all invites where the target is a specific user */ - @Json(name = "invitee") val invitee: String? = null + @Json(name = "invitee") val invitee: String? = null, + /** + * Capability advertisement. + */ + @Json(name= "capabilities") val capabilities: CallCapabilities? = null ): CallSignallingContent { @JsonClass(generateAdapter = true) 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 new file mode 100644 index 0000000000..4d08a7c925 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallReplacesContent.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2020 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.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent to signal the intent of a participant in a call to replace the call with another, + * such that the other participant ends up in a call with a new user. + */ +@JsonClass(generateAdapter = true) +data class CallReplacesContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, + /** + * An identifier for the call replacement itself, generated by the transferor. + */ + @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. + */ + @Json(name = "target_room") val targerRoomId: String? = null, + /** + * 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 + */ + @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. + */ + @Json(name = "await_call") val awaitCall: String? = null, + /** + * Required. The version of the VoIP specification this messages adheres to. + */ + @Json(name = "version") override val version: String? +): CallSignallingContent { + + @JsonClass(generateAdapter = true) + data class TargetUser( + /** + * Required. The matrix user ID of the transfer target + */ + @Json(name = "id") val id: String, + /** + * Optional. The display name of the transfer target. + */ + @Json(name = "display_name") val displayName: String, + /** + * 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/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index a6de65f9f5..4576675d4a 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,6 +24,7 @@ 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 @@ -185,6 +186,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa call.apply { opponentPartyId = Optional.from(content.partyId) opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + capabilities = content.capabilities ?: CallCapabilities() } 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 3c258df31a..e82d89fec6 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 @@ -16,7 +16,9 @@ package org.matrix.android.sdk.internal.session.call +import org.matrix.android.sdk.api.MatrixConfiguration 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.internal.di.DeviceId @@ -32,6 +34,7 @@ internal class MxCallFactory @Inject constructor( @DeviceId private val deviceId: String?, private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor, + private val matrixConfiguration: MatrixConfiguration, @UserId private val userId: String ) { @@ -46,10 +49,12 @@ internal class MxCallFactory @Inject constructor( opponentUserId = opponentUserId, isVideoCall = content.isVideo(), localEchoEventFactory = localEchoEventFactory, - eventSenderProcessor = eventSenderProcessor + eventSenderProcessor = eventSenderProcessor, + matrixConfiguration = matrixConfiguration ).apply { opponentPartyId = Optional.from(content.partyId) opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + capabilities = content.capabilities ?: CallCapabilities() } } @@ -63,7 +68,8 @@ internal class MxCallFactory @Inject constructor( opponentUserId = opponentUserId, isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, - eventSenderProcessor = eventSenderProcessor + eventSenderProcessor = eventSenderProcessor, + matrixConfiguration = matrixConfiguration ) } } 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 9368e94efc..c1375bae6a 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 @@ -16,6 +16,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.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.events.model.Content @@ -27,6 +28,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidate 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,8 +37,8 @@ import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerConten 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 -import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor 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 internal class MxCallImpl( @@ -48,11 +50,13 @@ internal class MxCallImpl( override val isVideoCall: Boolean, override val ourPartyId: String, private val localEchoEventFactory: LocalEchoEventFactory, - private val eventSenderProcessor: EventSenderProcessor + private val eventSenderProcessor: EventSenderProcessor, + private val matrixConfiguration: MatrixConfiguration ) : MxCall { override var opponentPartyId: Optional? = null override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION + override var capabilities: CallCapabilities? = null override var state: CallState = CallState.Idle set(value) { @@ -98,7 +102,8 @@ internal class MxCallImpl( partyId = ourPartyId, lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS, offer = CallInviteContent.Offer(sdp = sdpString), - version = MxCall.VOIP_PROTO_VERSION.toString() + version = MxCall.VOIP_PROTO_VERSION.toString(), + capabilities = buildCapabilities() ) .let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } @@ -158,7 +163,8 @@ internal class MxCallImpl( callId = callId, partyId = ourPartyId, answer = CallAnswerContent.Answer(sdp = sdpString), - version = MxCall.VOIP_PROTO_VERSION.toString() + version = MxCall.VOIP_PROTO_VERSION.toString(), + capabilities = buildCapabilities() ) .let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } @@ -203,4 +209,12 @@ internal class MxCallImpl( ) .also { localEchoEventFactory.createLocalEcho(it) } } + + private fun buildCapabilities(): CallCapabilities? { + return if (matrixConfiguration.supportsCallTransfer) { + CallCapabilities(true) + } else { + null + } + } }