diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt index 37ab198487..ff5dd4ffb5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallsListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt @@ -20,8 +20,9 @@ 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.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent -interface CallsListener { +interface CallListener { /** * Called when there is an incoming call within the room. */ @@ -39,5 +40,10 @@ interface CallsListener { */ fun onCallHangupReceived(callHangupContent: CallHangupContent) + /** + * Called when a called has been rejected + */ + fun onCallRejectReceived(callRejectContent: CallRejectContent) + fun onCallManagedByOtherSession(callId: String) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt index e28c1fa595..c6bdcd19c7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt @@ -28,9 +28,9 @@ interface CallSignalingService { */ fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall - fun addCallListener(listener: CallsListener) + fun addCallListener(listener: CallListener) - fun removeCallListener(listener: CallsListener) + fun removeCallListener(listener: CallListener) fun getCallWithId(callId: String): MxCall? 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 a1ab687894..1f09f18277 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 @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api.session.call +import org.matrix.android.sdk.api.util.Optional import org.webrtc.IceCandidate import org.webrtc.SessionDescription @@ -23,8 +24,12 @@ interface MxCallDetail { val callId: String val isOutgoing: Boolean val roomId: String - val otherUserId: String + val opponentUserId: String + val ourPartyId: String val isVideoCall: Boolean + + var opponentPartyId: Optional? + var opponentVersion: Int } /** @@ -32,6 +37,12 @@ interface MxCallDetail { */ interface MxCall : MxCallDetail { + companion object { + const val VOIP_PROTO_VERSION = 0 + } + + + var state: CallState /** @@ -42,9 +53,8 @@ interface MxCall : MxCallDetail { /** * Reject an incoming call - * It's an alias to hangUp */ - fun reject() = hangUp() + fun reject() /** * End the call 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 d6df2f36a4..6d2a0fbad5 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 @@ -27,11 +27,11 @@ data class CallAnswerContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, /** * Required. ID to let user identify remote echo of their own events */ - @Json(name = "party_id") val partyId: String? = null, + @Json(name = "party_id") override val partyId: String? = null, /** * Required. The session description object */ @@ -39,8 +39,8 @@ data class CallAnswerContent( /** * Required. The version of the VoIP specification this messages adheres to. This specification is version 0. */ - @Json(name = "version") val version: String? = "0" -) { + @Json(name = "version") override val version: String? = "0" +): CallSignallingContent { @JsonClass(generateAdapter = true) data class Answer( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt index d2a88a6793..8e48eed16f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallCandidatesContent.kt @@ -28,11 +28,11 @@ data class CallCandidatesContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, /** * Required. ID to let user identify remote echo of their own events */ - @Json(name = "party_id") val partyId: String? = null, + @Json(name = "party_id") override val partyId: String? = null, /** * Required. Array of objects describing the candidates. */ @@ -40,8 +40,8 @@ data class CallCandidatesContent( /** * Required. The version of the VoIP specification this messages adheres to. This specification is version 0. */ - @Json(name = "version") val version: String? = "0" -) { + @Json(name = "version") override val version: String? = "0" +): CallSignallingContent { @JsonClass(generateAdapter = true) data class Candidate( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt index d4a626d609..3e23ef0ef0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt @@ -28,22 +28,22 @@ data class CallHangupContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, /** * Required. ID to let user identify remote echo of their own events */ - @Json(name = "party_id") val partyId: String? = null, + @Json(name = "party_id") override val partyId: String? = null, /** * Required. The version of the VoIP specification this message adheres to. This specification is version 0. */ - @Json(name = "version") val version: String? = "0", + @Json(name = "version") override val version: String? = "0", /** * Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call. * When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails * or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"] */ @Json(name = "reason") val reason: Reason? = null -) { +) : CallSignallingContent { @JsonClass(generateAdapter = false) enum class Reason { @Json(name = "ice_failed") 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 c1e84b988c..4ee03a4a5a 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 @@ -27,11 +27,11 @@ data class CallInviteContent( /** * Required. A unique identifier for the call. */ - @Json(name = "call_id") val callId: String?, + @Json(name = "call_id") override val callId: String?, /** * Required. ID to let user identify remote echo of their own events */ - @Json(name = "party_id") val partyId: String? = null, + @Json(name = "party_id") override val partyId: String? = null, /** * Required. The session description object */ @@ -39,14 +39,14 @@ data class CallInviteContent( /** * Required. The version of the VoIP specification this message adheres to. This specification is version 0. */ - @Json(name = "version") val version: String? = "0", + @Json(name = "version") override val version: String? = "0", /** * Required. The time in milliseconds that the invite is valid for. * Once the invite age exceeds this value, clients should discard it. * They should also no longer show the call as awaiting an answer in the UI. */ @Json(name = "lifetime") val lifetime: Int? -) { +): CallSignallingContent { @JsonClass(generateAdapter = true) data class Offer( /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegociateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegociateContent.kt index be8ee1d9fc..efa3bdfb07 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegociateContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallNegociateContent.kt @@ -27,11 +27,11 @@ data class CallNegociateContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, /** * Required. ID to let user identify remote echo of their own events */ - @Json(name = "party_id") val partyId: String? = null, + @Json(name = "party_id") override val partyId: String? = null, /** * Required. The time in milliseconds that the negotiation is valid for. Once exceeded the sender * of the negotiate event should consider the negotiation failed (timed out) and the recipient should ignore it. @@ -41,7 +41,13 @@ data class CallNegociateContent( * Required. The session description object */ @Json(name = "description") val description: Description? = null, -) { + + /** + * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + */ + @Json(name = "version") override val version: String? = "0", + +): CallSignallingContent { @JsonClass(generateAdapter = true) data class Description( /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt index 96735b60bb..b8747803b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt @@ -28,13 +28,13 @@ data class CallRejectContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, /** * Required. ID to let user identify remote echo of their own events */ - @Json(name = "party_id") val partyId: String? = null, + @Json(name = "party_id") override val partyId: String? = null, /** * Required. The version of the VoIP specification this message adheres to. This specification is version 0. */ - @Json(name = "version") val version: String? = "0", -) + @Json(name = "version") override val version: String? = "0", +):CallSignallingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt index 9205be1e83..42ebed952e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSelectAnswerContent.kt @@ -27,13 +27,18 @@ data class CallSelectAnswerContent( /** * Required. The ID of the call this event relates to. */ - @Json(name = "call_id") val callId: String, + @Json(name = "call_id") override val callId: String, /** * Required. ID to let user identify remote echo of their own events */ - @Json(name = "party_id") val partyId: String? = null, + @Json(name = "party_id") override val partyId: String? = null, /** * Required. Indicates the answer user has chosen. */ @Json(name = "selected_party_id") val selectedPartyId: String? = null, -) + + /** + * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + */ + @Json(name = "version") override val version: String? = "0", +): CallSignallingContent diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt new file mode 100644 index 0000000000..e1c90f1952 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt @@ -0,0 +1,34 @@ +/* + * 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 + +interface CallSignallingContent { + /** + * Required. A unique identifier for the call. + */ + val callId: String? + + /** + * Required. ID to let user identify remote echo of their own events + */ + val partyId: String? + + /** + * Required. The version of the VoIP specification this message adheres to. This specification is version 0. + */ + val version: String? +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt new file mode 100644 index 0000000000..78437a2d69 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt @@ -0,0 +1,64 @@ +/* + * 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.internal.session.call + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.call.CallListener +import org.matrix.android.sdk.api.session.call.MxCall +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.CallHangupContent +import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent + +/** + * Dispatch each method safely to all listeners. + */ +class CallListenersDispatcher(private val listeners: Set) : CallListener { + + override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) = dispatch { + it.onCallInviteReceived(mxCall, callInviteContent) + } + + override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) = dispatch { + it.onCallIceCandidateReceived(mxCall, iceCandidatesContent) + } + + override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) = dispatch { + it.onCallAnswerReceived(callAnswerContent) + } + + override fun onCallHangupReceived(callHangupContent: CallHangupContent) = dispatch { + it.onCallHangupReceived(callHangupContent) + } + + override fun onCallRejectReceived(callRejectContent: CallRejectContent) = dispatch { + it.onCallRejectReceived(callRejectContent) + } + + override fun onCallManagedByOtherSession(callId: String) = dispatch { + it.onCallManagedByOtherSession(callId) + } + + private fun dispatch(lambda: (CallListener) -> Unit) { + listeners.toList().forEach { + tryOrNull { + lambda(it) + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt index 019da27d27..a903a0e218 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -18,10 +18,9 @@ package org.matrix.android.sdk.internal.session.call import android.os.SystemClock import org.matrix.android.sdk.api.MatrixCallback -import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallSignalingService import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.CallsListener import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.api.session.events.model.Event @@ -31,16 +30,21 @@ 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.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.call.model.MxCallImpl -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 org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import timber.log.Timber +import java.math.BigDecimal import java.util.UUID import javax.inject.Inject @@ -48,6 +52,8 @@ import javax.inject.Inject internal class DefaultCallSignalingService @Inject constructor( @UserId private val userId: String, + @DeviceId + private val deviceId: String?, private val activeCallHandler: ActiveCallHandler, private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor, @@ -55,7 +61,8 @@ internal class DefaultCallSignalingService @Inject constructor( private val turnServerTask: GetTurnServerTask ) : CallSignalingService { - private val callListeners = mutableSetOf() + private val callListeners = mutableSetOf() + private val callListenersDispatcher = CallListenersDispatcher(callListeners) private val cachedTurnServerResponse = object { // Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it. @@ -100,7 +107,8 @@ internal class DefaultCallSignalingService @Inject constructor( isOutgoing = true, roomId = roomId, userId = userId, - otherUserId = otherUserId, + ourPartyId = deviceId ?: "", + opponentUserId = otherUserId, isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, eventSenderProcessor = eventSenderProcessor @@ -110,11 +118,11 @@ internal class DefaultCallSignalingService @Inject constructor( } } - override fun addCallListener(listener: CallsListener) { + override fun addCallListener(listener: CallListener) { callListeners.add(listener) } - override fun removeCallListener(listener: CallsListener) { + override fun removeCallListener(listener: CallListener) { callListeners.remove(listener) } @@ -129,125 +137,115 @@ internal class DefaultCallSignalingService @Inject constructor( internal fun onCallEvent(event: Event) { when (event.getClearType()) { - EventType.CALL_ANSWER -> { - event.getClearContent().toModel()?.let { - if (event.senderId == userId) { - // ok it's an answer from me.. is it remote echo or other session - val knownCall = getCallWithId(it.callId) - if (knownCall == null) { - Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${it.callId} send by me") - } else if (!knownCall.isOutgoing) { - // incoming call - // if it was anwsered by this session, the call state would be in Answering(or connected) state - if (knownCall.state == CallState.LocalRinging) { - // discard current call, it's answered by another of my session - onCallManageByOtherSession(it.callId) - } - } - return - } - - onCallAnswer(it) - } + EventType.CALL_ANSWER -> { + handleCallAnswerEvent(event) } - EventType.CALL_INVITE -> { - if (event.senderId == userId) { - // Always ignore local echos of invite - return - } - - event.getClearContent().toModel()?.let { content -> - val incomingCall = MxCallImpl( - callId = content.callId ?: return@let, - isOutgoing = false, - roomId = event.roomId ?: return@let, - userId = userId, - otherUserId = event.senderId ?: return@let, - isVideoCall = content.isVideo(), - localEchoEventFactory = localEchoEventFactory, - eventSenderProcessor = eventSenderProcessor - ) - activeCallHandler.addCall(incomingCall) - onCallInvite(incomingCall, content) - } + EventType.CALL_INVITE -> { + handleCallInviteEvent(event) } - EventType.CALL_HANGUP -> { - event.getClearContent().toModel()?.let { content -> - - if (event.senderId == userId) { - // ok it's an answer from me.. is it remote echo or other session - val knownCall = getCallWithId(content.callId) - if (knownCall == null) { - Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${content.callId} send by me") - } else if (!knownCall.isOutgoing) { - // incoming call - if (knownCall.state == CallState.LocalRinging) { - // discard current call, it's answered by another of my session - onCallManageByOtherSession(content.callId) - } - } - return - } - - activeCallHandler.removeCall(content.callId) - onCallHangup(content) - } + EventType.CALL_HANGUP -> { + handleCallHangupEvent(event) + } + EventType.CALL_REJECT -> { + handleCallRejectEvent(event) } EventType.CALL_CANDIDATES -> { - if (event.senderId == userId) { - // Always ignore local echos of invite - return - } - event.getClearContent().toModel()?.let { content -> - activeCallHandler.getCallWithId(content.callId)?.let { - onCallIceCandidate(it, content) - } - } + handleCallCandidatesEvent(event) } } } - private fun onCallHangup(hangup: CallHangupContent) { - callListeners.toList().forEach { - tryOrNull { - it.onCallHangupReceived(hangup) - } + private fun handleCallCandidatesEvent(event: Event) { + val content = event.getClearContent().toModel() ?: return + val call = content.getCall() ?: return + if (call.ourPartyId == content.partyId) { + // Ignore remote echo + return + } + if (call.opponentPartyId != Optional.from(content.partyId)) { + Timber.v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}") + return + } + callListenersDispatcher.onCallIceCandidateReceived(call, content) + } + + private fun handleCallRejectEvent(event: Event) { + val content = event.getClearContent().toModel() ?: return + val call = content.getCall() ?: return + activeCallHandler.removeCall(content.callId) + // No need to check party_id for reject because if we'd received either + // an answer or reject, we wouldn't be in state InviteSent + if (call.state != CallState.Dialing) { + return + } + callListenersDispatcher.onCallRejectReceived(content) + } + + private fun handleCallHangupEvent(event: Event) { + val content = event.getClearContent().toModel() ?: return + val call = content.getCall() ?: return + if (call.state != CallState.Terminated) { + // Need to check for party_id? + activeCallHandler.removeCall(content.callId) + callListenersDispatcher.onCallHangupReceived(content) } } - private fun onCallAnswer(answer: CallAnswerContent) { - callListeners.toList().forEach { - tryOrNull { - it.onCallAnswerReceived(answer) + private fun handleCallInviteEvent(event: Event) { + val content = event.getClearContent().toModel() ?: return + if (content.partyId == deviceId) { + // Ignore remote echo + return + } + val incomingCall = MxCallImpl( + callId = content.callId ?: return, + isOutgoing = false, + roomId = event.roomId ?: return, + userId = userId, + ourPartyId = deviceId ?: "", + opponentUserId = event.senderId ?: return, + isVideoCall = content.isVideo(), + localEchoEventFactory = localEchoEventFactory, + eventSenderProcessor = eventSenderProcessor + ).apply { + opponentPartyId = Optional.from(content.partyId) + opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION + } + activeCallHandler.addCall(incomingCall) + callListenersDispatcher.onCallInviteReceived(incomingCall, content) + } + + private fun handleCallAnswerEvent(event: Event) { + val content = event.getClearContent().toModel() ?: return + val call = content.getCall() ?: return + if (call.ourPartyId == content.partyId) { + // Ignore remote echo + return + } + if (event.senderId == userId) { + // discard current call, it's answered by another of my session + callListenersDispatcher.onCallManagedByOtherSession(content.callId) + } else { + if (call.opponentPartyId != null) { + 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 + } + callListenersDispatcher.onCallAnswerReceived(content) } } - private fun onCallManageByOtherSession(callId: String) { - callListeners.toList().forEach { - tryOrNull { - it.onCallManagedByOtherSession(callId) - } + private fun CallSignallingContent.getCall(): MxCall? { + val currentCall = callId?.let { + activeCallHandler.getCallWithId(it) } - } - - private fun onCallInvite(incomingCall: MxCall, invite: CallInviteContent) { - // Ignore the invitation from current user - if (incomingCall.otherUserId == userId) return - - callListeners.toList().forEach { - tryOrNull { - it.onCallInviteReceived(incomingCall, invite) - } - } - } - - private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) { - callListeners.toList().forEach { - tryOrNull { - it.onCallIceCandidateReceived(incomingCall, candidates) - } + if (currentCall == null) { + Timber.v("Call for content: $this is null") } + return currentCall } companion object { 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 6c0d437a60..126a527f88 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 @@ -28,6 +28,8 @@ 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.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent +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 @@ -40,12 +42,16 @@ internal class MxCallImpl( override val isOutgoing: Boolean, override val roomId: String, private val userId: String, - override val otherUserId: String, + override val opponentUserId: String, override val isVideoCall: Boolean, + override val ourPartyId: String, private val localEchoEventFactory: LocalEchoEventFactory, private val eventSenderProcessor: EventSenderProcessor ) : MxCall { + override var opponentPartyId: Optional? = null + override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION + override var state: CallState = CallState.Idle set(value) { field = value @@ -87,6 +93,7 @@ internal class MxCallImpl( state = CallState.Dialing CallInviteContent( callId = callId, + partyId = ourPartyId, lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS, offer = CallInviteContent.Offer(sdp = sdp.description) ) @@ -97,6 +104,7 @@ internal class MxCallImpl( override fun sendLocalIceCandidates(candidates: List) { CallCandidatesContent( callId = callId, + partyId = ourPartyId, candidates = candidates.map { CallCandidatesContent.Candidate( sdpMid = it.sdpMid, @@ -113,10 +121,28 @@ internal class MxCallImpl( // For now we don't support this flow } + override fun reject() { + if(opponentVersion < 1){ + Timber.v("Opponent version is less than 1 (${opponentVersion}): sending hangup instead of reject") + hangUp() + return + } + Timber.v("## VOIP reject $callId") + CallRejectContent( + callId = callId, + partyId = ourPartyId, + version = MxCall.VOIP_PROTO_VERSION.toString() + ) + .let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) } + .also { eventSenderProcessor.postEvent(it) } + state = CallState.Terminated + } + override fun hangUp() { Timber.v("## VOIP hangup $callId") CallHangupContent( - callId = callId + callId = callId, + partyId = ourPartyId, ) .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) } .also { eventSenderProcessor.postEvent(it) } @@ -129,6 +155,7 @@ internal class MxCallImpl( state = CallState.Answering CallAnswerContent( callId = callId, + partyId = ourPartyId, answer = CallAnswerContent.Answer(sdp = sdp.description) ) .let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) } @@ -147,4 +174,5 @@ internal class MxCallImpl( ) .also { localEchoEventFactory.createLocalEcho(it) } } + } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 9ab39bc0a9..24b3e5d843 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -375,7 +375,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis return Intent(context, VectorCallActivity::class.java).apply { // what could be the best flags? flags = Intent.FLAG_ACTIVITY_NEW_TASK - putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall)) + putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.opponentUserId, !mxCall.isOutgoing, mxCall.isVideoCall)) putExtra(EXTRA_MODE, OUTGOING_CREATED) } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 445f40e5b1..014cab6765 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -136,8 +136,8 @@ class VectorCallViewModel @AssistedInject constructor( session.callSignalingService().getCallWithId(it)?.let { mxCall -> this.call = mxCall - mxCall.otherUserId - val item: MatrixItem? = session.getUser(mxCall.otherUserId)?.toMatrixItem() + mxCall.opponentUserId + val item: MatrixItem? = session.getUser(mxCall.opponentUserId)?.toMatrixItem() mxCall.addListener(callStateListener) diff --git a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt index 86b38c1158..998c4da536 100644 --- a/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/WebRtcPeerConnectionManager.kt @@ -34,7 +34,7 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.CallState -import org.matrix.android.sdk.api.session.call.CallsListener +import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.EglUtils import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.TurnServerResponse @@ -42,6 +42,7 @@ 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.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent +import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent import org.webrtc.AudioSource import org.webrtc.AudioTrack import org.webrtc.Camera1Enumerator @@ -76,7 +77,7 @@ import javax.inject.Singleton class WebRtcPeerConnectionManager @Inject constructor( private val context: Context, private val activeSessionDataSource: ActiveSessionDataSource -) : CallsListener, LifecycleObserver { +) : CallListener, LifecycleObserver { private val currentSession: Session? get() = activeSessionDataSource.currentValue?.orNull() @@ -330,7 +331,7 @@ class WebRtcPeerConnectionManager @Inject constructor( currentCall?.mxCall ?.takeIf { it.state is CallState.Connected } ?.let { mxCall -> - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() + val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() ?: mxCall.roomId // Start background service with notification CallService.onPendingCall( @@ -388,7 +389,7 @@ class WebRtcPeerConnectionManager @Inject constructor( val mxCall = callContext.mxCall // Update service state - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() + val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() ?: mxCall.roomId CallService.onPendingCall( context = context, @@ -576,8 +577,8 @@ class WebRtcPeerConnectionManager @Inject constructor( ?.let { mxCall -> // Start background service with notification - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() - ?: mxCall.otherUserId + val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() + ?: mxCall.opponentUserId CallService.onOnGoingCallBackground( context = context, isVideo = mxCall.isVideoCall, @@ -650,8 +651,8 @@ class WebRtcPeerConnectionManager @Inject constructor( callAudioManager.startForCall(createdCall) currentCall = callContext - val name = currentSession?.getUser(createdCall.otherUserId)?.getBestName() - ?: createdCall.otherUserId + val name = currentSession?.getUser(createdCall.opponentUserId)?.getBestName() + ?: createdCall.opponentUserId CallService.onOutgoingCallRinging( context = context.applicationContext, isVideo = createdCall.isVideoCall, @@ -706,8 +707,8 @@ class WebRtcPeerConnectionManager @Inject constructor( } // Start background service with notification - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() - ?: mxCall.otherUserId + val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() + ?: mxCall.opponentUserId CallService.onIncomingCallRinging( context = context, isVideo = mxCall.isVideoCall, @@ -845,8 +846,8 @@ class WebRtcPeerConnectionManager @Inject constructor( } val mxCall = call.mxCall // Update service state - val name = currentSession?.getUser(mxCall.otherUserId)?.getBestName() - ?: mxCall.otherUserId + val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() + ?: mxCall.opponentUserId CallService.onPendingCall( context = context, isVideo = mxCall.isVideoCall, @@ -873,6 +874,16 @@ class WebRtcPeerConnectionManager @Inject constructor( endCall(false) } + override fun onCallRejectReceived(callRejectContent: CallRejectContent) { + val call = currentCall ?: return + // Remote echos are filtered, so it's only remote hangups that i will get here + if (call.mxCall.callId != callRejectContent.callId) return Unit.also { + Timber.w("onCallRejected for non active call? ${callRejectContent.callId}") + } + call.mxCall.state = CallState.Terminated + endCall(false) + } + override fun onCallManagedByOtherSession(callId: String) { Timber.v("## VOIP onCallManagedByOtherSession: $callId") currentCall = null diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 1c63d63ae0..59f81d3436 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -332,7 +332,7 @@ class HomeDetailFragment @Inject constructor( context = requireContext(), callId = call.callId, roomId = call.roomId, - otherUserId = call.otherUserId, + otherUserId = call.opponentUserId, isIncomingCall = !call.isOutgoing, isVideoCall = call.isVideoCall, mode = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 9c6c473a7f..2566032e78 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1962,7 +1962,7 @@ class RoomDetailFragment @Inject constructor( context = requireContext(), callId = call.callId, roomId = call.roomId, - otherUserId = call.otherUserId, + otherUserId = call.opponentUserId, isIncomingCall = !call.isOutgoing, isVideoCall = call.isVideoCall, mode = null