From ba11ca0e9dbe39b7593c57d2e2012ba7824da9d1 Mon Sep 17 00:00:00 2001
From: ganfra <francoisg@matrix.org>
Date: Fri, 13 Nov 2020 11:53:22 +0100
Subject: [PATCH] VoIP: add partyId and handle version as string

---
 .../{CallsListener.kt => CallListener.kt}     |   8 +-
 .../api/session/call/CallSignalingService.kt  |   4 +-
 .../android/sdk/api/session/call/MxCall.kt    |  16 +-
 .../room/model/call/CallAnswerContent.kt      |   8 +-
 .../room/model/call/CallCandidatesContent.kt  |   8 +-
 .../room/model/call/CallHangupContent.kt      |   8 +-
 .../room/model/call/CallInviteContent.kt      |   8 +-
 .../room/model/call/CallNegociateContent.kt   |  12 +-
 .../room/model/call/CallRejectContent.kt      |   8 +-
 .../model/call/CallSelectAnswerContent.kt     |  11 +-
 .../room/model/call/CallSignallingContent.kt  |  34 +++
 .../session/call/CallListenersDispatcher.kt   |  64 ++++++
 .../call/DefaultCallSignalingService.kt       | 216 +++++++++---------
 .../internal/session/call/model/MxCallImpl.kt |  32 ++-
 .../app/features/call/VectorCallActivity.kt   |   2 +-
 .../app/features/call/VectorCallViewModel.kt  |   4 +-
 .../call/WebRtcPeerConnectionManager.kt       |  35 ++-
 .../app/features/home/HomeDetailFragment.kt   |   2 +-
 .../home/room/detail/RoomDetailFragment.kt    |   2 +-
 19 files changed, 322 insertions(+), 160 deletions(-)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/{CallsListener.kt => CallListener.kt} (86%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallSignallingContent.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt

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<String>?
+    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>) : 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<CallsListener>()
+    private val callListeners = mutableSetOf<CallListener>()
+    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<CallAnswerContent>()?.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<CallInviteContent>()?.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<CallHangupContent>()?.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<CallCandidatesContent>()?.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<CallCandidatesContent>() ?: 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<CallRejectContent>() ?: 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<CallHangupContent>() ?: 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<CallInviteContent>() ?: 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<CallAnswerContent>() ?: 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<String>? = 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<IceCandidate>) {
         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