Ver Fonte

Merge pull request #4120 from nextcloud/add-support-for-federated-calls

Add support for federated calls
Marcel Hibbe há 9 meses atrás
pai
commit
580dd584e5
20 ficheiros alterados com 291 adições e 27 exclusões
  1. 12 2
      app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
  2. 23 3
      app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java
  3. 5 0
      app/src/main/java/com/nextcloud/talk/call/CallParticipant.java
  4. 2 0
      app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java
  5. 16 0
      app/src/main/java/com/nextcloud/talk/call/CallParticipantModel.java
  6. 7 0
      app/src/main/java/com/nextcloud/talk/call/MutableCallParticipantModel.java
  7. 57 6
      app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
  8. 5 2
      app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt
  9. 24 0
      app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationHelloAuthParams.kt
  10. 30 0
      app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationSettings.kt
  11. 4 2
      app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt
  12. 28 0
      app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomFederationWebSocketMessage.kt
  13. 4 2
      app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomWebSocketMessage.kt
  14. 15 0
      app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java
  15. 4 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt
  16. 16 1
      app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java
  17. 17 3
      app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt
  18. 4 0
      app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java
  19. 4 0
      app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java
  20. 14 6
      app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java

+ 12 - 2
app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt

@@ -1453,7 +1453,7 @@ class CallActivity : CallBaseActivity() {
     private fun fetchSignalingSettings() {
         Log.d(TAG, "fetchSignalingSettings")
         val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser, intArrayOf(ApiUtils.API_V3, 2, 1))
-        ncApi!!.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(apiVersion, baseUrl))
+        ncApi!!.getSignalingSettings(credentials, ApiUtils.getUrlForSignalingSettings(apiVersion, baseUrl, roomToken!!))
             .subscribeOn(Schedulers.io())
             .retry(API_RETRIES)
             .observeOn(AndroidSchedulers.mainThread())
@@ -1475,6 +1475,8 @@ class CallActivity : CallBaseActivity() {
                                 signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer
                             externalSignalingServer!!.externalSignalingTicket =
                                 signalingSettingsOverall.ocs!!.settings!!.externalSignalingTicket
+                            externalSignalingServer!!.federation =
+                                signalingSettingsOverall.ocs!!.settings!!.federation
                             hasExternalSignalingServer = true
                         } else {
                             hasExternalSignalingServer = false
@@ -1630,7 +1632,9 @@ class CallActivity : CallBaseActivity() {
 
     private fun callOrJoinRoomViaWebSocket() {
         if (hasExternalSignalingServer) {
-            webSocketClient!!.joinRoomWithRoomTokenAndSession(roomToken!!, callSession)
+            webSocketClient!!.joinRoomWithRoomTokenAndSession(
+                roomToken!!, callSession, externalSignalingServer!!.federation
+            )
         } else {
             performCall()
         }
@@ -2157,6 +2161,10 @@ class CallActivity : CallBaseActivity() {
             Log.d(TAG, "   newSession joined: $sessionId")
             addCallParticipant(sessionId)
 
+            if (participant.actorType != null && participant.actorId != null) {
+                callParticipants[sessionId]!!.setActor(participant.actorType, participant.actorId)
+            }
+
             val userId = participant.userId
             if (userId != null) {
                 callParticipants[sessionId]!!.setUserId(userId)
@@ -2510,10 +2518,12 @@ class CallActivity : CallBaseActivity() {
         }
         val defaultGuestNick = resources.getString(R.string.nc_nick_guest)
         val participantDisplayItem = ParticipantDisplayItem(
+            context,
             baseUrl,
             defaultGuestNick,
             rootEglBase,
             videoStreamType,
+            roomToken,
             callParticipantModel
         )
         val sessionId = callParticipantModel.sessionId

+ 23 - 3
app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java

@@ -8,13 +8,16 @@
  */
 package com.nextcloud.talk.adapters;
 
+import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.text.TextUtils;
 
 import com.nextcloud.talk.call.CallParticipantModel;
 import com.nextcloud.talk.call.RaisedHand;
+import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.utils.ApiUtils;
+import com.nextcloud.talk.utils.DisplayUtils;
 
 import org.webrtc.EglBase;
 import org.webrtc.MediaStream;
@@ -29,6 +32,8 @@ public class ParticipantDisplayItem {
 
     private final ParticipantDisplayItemNotifier participantDisplayItemNotifier = new ParticipantDisplayItemNotifier();
 
+    private final Context context;
+
     private final String baseUrl;
     private final String defaultGuestNick;
     private final EglBase rootEglBase;
@@ -36,8 +41,12 @@ public class ParticipantDisplayItem {
     private final String session;
     private final String streamType;
 
+    private final String roomToken;
+
     private final CallParticipantModel callParticipantModel;
 
+    private Participant.ActorType actorType;
+    private String actorId;
     private String userId;
     private PeerConnection.IceConnectionState iceConnectionState;
     private String nick;
@@ -62,8 +71,10 @@ public class ParticipantDisplayItem {
         }
     };
 
-    public ParticipantDisplayItem(String baseUrl, String defaultGuestNick, EglBase rootEglBase, String streamType,
-                                  CallParticipantModel callParticipantModel) {
+    public ParticipantDisplayItem(Context context, String baseUrl, String defaultGuestNick, EglBase rootEglBase,
+                                  String streamType, String roomToken, CallParticipantModel callParticipantModel) {
+        this.context = context;
+
         this.baseUrl = baseUrl;
         this.defaultGuestNick = defaultGuestNick;
         this.rootEglBase = rootEglBase;
@@ -71,6 +82,8 @@ public class ParticipantDisplayItem {
         this.session = callParticipantModel.getSessionId();
         this.streamType = streamType;
 
+        this.roomToken = roomToken;
+
         this.callParticipantModel = callParticipantModel;
         this.callParticipantModel.addObserver(callParticipantModelObserver, handler);
 
@@ -82,6 +95,8 @@ public class ParticipantDisplayItem {
     }
 
     private void updateFromModel() {
+        actorType = callParticipantModel.getActorType();
+        actorId = callParticipantModel.getActorId();
         userId = callParticipantModel.getUserId();
         nick = callParticipantModel.getNick();
 
@@ -107,7 +122,10 @@ public class ParticipantDisplayItem {
     }
 
     private void updateUrlForAvatar() {
-        if (!TextUtils.isEmpty(userId)) {
+        if (actorType == Participant.ActorType.FEDERATED) {
+            int darkTheme = DisplayUtils.INSTANCE.isDarkModeOn(context) ? 1 : 0;
+            urlForAvatar = ApiUtils.getUrlForFederatedAvatar(baseUrl, roomToken, actorId, darkTheme, true);
+        } else if (!TextUtils.isEmpty(userId)) {
             urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
         } else {
             urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, getNick(), true);
@@ -166,6 +184,8 @@ public class ParticipantDisplayItem {
     public String toString() {
         return "ParticipantSession{" +
                 "userId='" + userId + '\'' +
+                ", actorType='" + actorType + '\'' +
+                ", actorId='" + actorId + '\'' +
                 ", session='" + session + '\'' +
                 ", nick='" + nick + '\'' +
                 ", urlForAvatar='" + urlForAvatar + '\'' +

+ 5 - 0
app/src/main/java/com/nextcloud/talk/call/CallParticipant.java

@@ -6,6 +6,7 @@
  */
 package com.nextcloud.talk.call;
 
+import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.signaling.SignalingMessageReceiver;
 import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
 
@@ -132,6 +133,10 @@ public class CallParticipant {
         return callParticipantModel;
     }
 
+    public void setActor(Participant.ActorType actorType, String actorId) {
+        callParticipantModel.setActor(actorType, actorId);
+    }
+
     public void setUserId(String userId) {
         callParticipantModel.setUserId(userId);
     }

+ 2 - 0
app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java

@@ -122,6 +122,8 @@ public class CallParticipantList {
 
         private Participant copyParticipant(Participant participant) {
             Participant copiedParticipant = new Participant();
+            copiedParticipant.setActorId(participant.getActorId());
+            copiedParticipant.setActorType(participant.getActorType());
             copiedParticipant.setInCall(participant.getInCall());
             copiedParticipant.setInternal(participant.getInternal());
             copiedParticipant.setLastPing(participant.getLastPing());

+ 16 - 0
app/src/main/java/com/nextcloud/talk/call/CallParticipantModel.java

@@ -8,6 +8,8 @@ package com.nextcloud.talk.call;
 
 import android.os.Handler;
 
+import com.nextcloud.talk.models.json.participants.Participant;
+
 import org.webrtc.MediaStream;
 import org.webrtc.PeerConnection;
 
@@ -25,6 +27,8 @@ import java.util.Objects;
  *
  * Audio and video in screen shares, on the other hand, are always seen as available.
  *
+ * Actor type and actor id will be set only in Talk >= 20.
+ *
  * Clients of the model can observe it with CallParticipantModel.Observer to be notified when any value changes.
  * Getters called after receiving a notification are guaranteed to provide at least the value that triggered the
  * notification, but it may return even a more up to date one (so getting the value again on the following
@@ -39,6 +43,8 @@ public class CallParticipantModel {
 
     protected final String sessionId;
 
+    protected Data<Participant.ActorType> actorType;
+    protected Data<String> actorId;
     protected Data<String> userId;
     protected Data<String> nick;
 
@@ -81,6 +87,8 @@ public class CallParticipantModel {
     public CallParticipantModel(String sessionId) {
         this.sessionId = sessionId;
 
+        this.actorType = new Data<>();
+        this.actorId = new Data<>();
         this.userId = new Data<>();
         this.nick = new Data<>();
 
@@ -101,6 +109,14 @@ public class CallParticipantModel {
         return sessionId;
     }
 
+    public Participant.ActorType getActorType() {
+        return actorType.getValue();
+    }
+
+    public String getActorId() {
+        return actorId.getValue();
+    }
+
     public String getUserId() {
         return userId.getValue();
     }

+ 7 - 0
app/src/main/java/com/nextcloud/talk/call/MutableCallParticipantModel.java

@@ -6,6 +6,8 @@
  */
 package com.nextcloud.talk.call;
 
+import com.nextcloud.talk.models.json.participants.Participant;
+
 import org.webrtc.MediaStream;
 import org.webrtc.PeerConnection;
 
@@ -20,6 +22,11 @@ public class MutableCallParticipantModel extends CallParticipantModel {
         super(sessionId);
     }
 
+    public void setActor(Participant.ActorType actorType, String actorId) {
+        this.actorType.setValue(actorType);
+        this.actorId.setValue(actorId);
+    }
+
     public void setUserId(String userId) {
         this.userId.setValue(userId);
     }

+ 57 - 6
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

@@ -120,10 +120,12 @@ import com.nextcloud.talk.jobs.ShareOperationWorker
 import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
 import com.nextcloud.talk.location.LocationPickerActivity
 import com.nextcloud.talk.messagesearch.MessageSearchActivity
+import com.nextcloud.talk.models.ExternalSignalingServer
 import com.nextcloud.talk.models.domain.ConversationModel
 import com.nextcloud.talk.models.json.capabilities.SpreedCapability
 import com.nextcloud.talk.models.json.chat.ReadStatus
 import com.nextcloud.talk.models.json.conversations.ConversationEnums
+import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
 import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
 import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
 import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
@@ -177,6 +179,8 @@ import com.stfalcon.chatkit.messages.MessageHolders
 import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker
 import com.stfalcon.chatkit.messages.MessagesListAdapter
 import com.stfalcon.chatkit.utils.DateFormatter
+import io.reactivex.Observer
+import io.reactivex.disposables.Disposable
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.collect
@@ -302,6 +306,7 @@ class ChatActivity :
 
     var webSocketInstance: WebSocketInstance? = null
     var signalingMessageSender: SignalingMessageSender? = null
+    var externalSignalingServer: ExternalSignalingServer? = null
 
     var getRoomInfoTimerHandler: Handler? = null
 
@@ -631,10 +636,12 @@ class ChatActivity :
 
                     logConversationInfos("joinRoomWithPassword#onNext")
 
+                    setupWebsocket()
                     if (webSocketInstance != null) {
                         webSocketInstance?.joinRoomWithRoomTokenAndSession(
                             roomToken,
-                            sessionIdAfterRoomJoined
+                            sessionIdAfterRoomJoined,
+                            externalSignalingServer?.federation
                         )
                     }
                     if (startCallFromNotification != null && startCallFromNotification) {
@@ -952,7 +959,6 @@ class ChatActivity :
 
         pullChatMessagesPending = false
 
-        setupWebsocket()
         webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
         webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
 
@@ -2391,10 +2397,12 @@ class ChatActivity :
         } else {
             Log.d(TAG, "sessionID was valid -> skip joinRoom")
 
+            setupWebsocket()
             if (webSocketInstance != null) {
                 webSocketInstance?.joinRoomWithRoomTokenAndSession(
                     roomToken,
-                    sessionIdAfterRoomJoined
+                    sessionIdAfterRoomJoined,
+                    externalSignalingServer?.federation
                 )
             }
         }
@@ -2423,16 +2431,59 @@ class ChatActivity :
     }
 
     private fun setupWebsocket() {
-        if (conversationUser == null) {
+        if (currentConversation == null || conversationUser == null) {
             return
         }
-        webSocketInstance = WebSocketConnectionHelper.getWebSocketInstanceForUser(conversationUser!!)
+
+        if (currentConversation!!.remoteServer != null) {
+            val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V3, 2, 1))
+            ncApi!!.getSignalingSettings(
+                credentials,
+                ApiUtils.getUrlForSignalingSettings(apiVersion, conversationUser!!.baseUrl, roomToken!!)
+            ).blockingSubscribe(object : Observer<SignalingSettingsOverall> {
+                override fun onSubscribe(d: Disposable) {
+                    // unused atm
+                }
+
+                override fun onNext(signalingSettingsOverall: SignalingSettingsOverall) {
+                    if (signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer == null) {
+                        return
+                    }
+
+                    externalSignalingServer = ExternalSignalingServer()
+                    externalSignalingServer!!.externalSignalingServer = signalingSettingsOverall.ocs!!.settings!!
+                        .externalSignalingServer
+                    externalSignalingServer!!.externalSignalingTicket = signalingSettingsOverall.ocs!!.settings!!
+                        .externalSignalingTicket
+                    externalSignalingServer!!.federation = signalingSettingsOverall.ocs!!.settings!!.federation
+
+                    webSocketInstance = WebSocketConnectionHelper.getExternalSignalingInstanceForServer(
+                        externalSignalingServer!!.externalSignalingServer,
+                        conversationUser,
+                        externalSignalingServer!!.externalSignalingTicket,
+                        TextUtils.isEmpty(credentials)
+                    )
+                }
+
+                override fun onError(e: Throwable) {
+                    Log.e(CallActivity.TAG, e.message, e)
+                }
+
+                override fun onComplete() {
+                    // unused atm
+                }
+            })
+        } else {
+            webSocketInstance = WebSocketConnectionHelper.getWebSocketInstanceForUser(conversationUser!!)
+        }
 
         if (webSocketInstance == null) {
             Log.d(TAG, "webSocketInstance not set up. This should only happen when not using the HPB")
         }
 
         signalingMessageSender = webSocketInstance?.signalingMessageSender
+        webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
+        webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
     }
 
     private fun processCallStartedMessages(chatMessageList: List<ChatMessage>) {
@@ -3190,7 +3241,7 @@ class ChatActivity :
             val lon = data["longitude"]!!
             metaData =
                 "{\"type\":\"geo-location\",\"id\":\"geo:$lat,$lon\",\"latitude\":\"$lat\"," +
-                "\"longitude\":\"$lon\",\"name\":\"$name\"}"
+                    "\"longitude\":\"$lon\",\"name\":\"$name\"}"
         }
 
         when (type) {

+ 5 - 2
app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt

@@ -10,6 +10,7 @@ package com.nextcloud.talk.models
 import android.os.Parcelable
 import com.bluelinelabs.logansquare.annotation.JsonField
 import com.bluelinelabs.logansquare.annotation.JsonObject
+import com.nextcloud.talk.models.json.signaling.settings.FederationSettings
 import kotlinx.parcelize.Parcelize
 
 @Parcelize
@@ -18,8 +19,10 @@ data class ExternalSignalingServer(
     @JsonField(name = ["externalSignalingServer"])
     var externalSignalingServer: String? = null,
     @JsonField(name = ["externalSignalingTicket"])
-    var externalSignalingTicket: String? = null
+    var externalSignalingTicket: String? = null,
+    @JsonField(name = ["federation"])
+    var federation: FederationSettings? = null
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
-    constructor() : this(null, null)
+    constructor() : this(null, null, null)
 }

+ 24 - 0
app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationHelloAuthParams.kt

@@ -0,0 +1,24 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.models.json.signaling.settings
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+
+@Parcelize
+@JsonObject
+@Serializable
+data class FederationHelloAuthParams(
+    @JsonField(name = ["token"])
+    var token: String? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null)
+}

+ 30 - 0
app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/FederationSettings.kt

@@ -0,0 +1,30 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.models.json.signaling.settings
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.parcelize.Parcelize
+import kotlinx.serialization.Serializable
+
+@Parcelize
+@JsonObject
+@Serializable
+data class FederationSettings(
+    @JsonField(name = ["server"])
+    var server: String? = null,
+    @JsonField(name = ["nextcloudServer"])
+    var nextcloudServer: String? = null,
+    @JsonField(name = ["helloAuthParams"])
+    var helloAuthParams: FederationHelloAuthParams? = null,
+    @JsonField(name = ["roomId"])
+    var roomId: String? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null, null, null)
+}

+ 4 - 2
app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt

@@ -24,8 +24,10 @@ data class SignalingSettings(
     @JsonField(name = ["server"])
     var externalSignalingServer: String? = null,
     @JsonField(name = ["ticket"])
-    var externalSignalingTicket: String? = null
+    var externalSignalingTicket: String? = null,
+    @JsonField(name = ["federation"])
+    var federation: FederationSettings? = null
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
-    constructor() : this(null, null, null)
+    constructor() : this(null, null, null, null)
 }

+ 28 - 0
app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomFederationWebSocketMessage.kt

@@ -0,0 +1,28 @@
+/*
+ * Nextcloud Talk - Android Client
+ *
+ * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package com.nextcloud.talk.models.json.websocket
+
+import android.os.Parcelable
+import com.bluelinelabs.logansquare.annotation.JsonField
+import com.bluelinelabs.logansquare.annotation.JsonObject
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+@JsonObject
+class RoomFederationWebSocketMessage(
+    @JsonField(name = ["signaling"])
+    var signaling: String? = null,
+    @JsonField(name = ["url"])
+    var url: String? = null,
+    @JsonField(name = ["roomid"])
+    var roomid: String? = null,
+    @JsonField(name = ["token"])
+    var token: String? = null
+) : Parcelable {
+    // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
+    constructor() : this(null, null, null, null)
+}

+ 4 - 2
app/src/main/java/com/nextcloud/talk/models/json/websocket/RoomWebSocketMessage.kt

@@ -20,8 +20,10 @@ class RoomWebSocketMessage(
     @JsonField(name = ["sessionid"])
     var sessionId: String? = null,
     @JsonField(name = ["properties"])
-    var roomPropertiesWebSocketMessage: RoomPropertiesWebSocketMessage? = null
+    var roomPropertiesWebSocketMessage: RoomPropertiesWebSocketMessage? = null,
+    @JsonField(name = ["federation"])
+    var roomFederationWebSocketMessage: RoomFederationWebSocketMessage? = null
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
-    constructor() : this(null, null, null)
+    constructor() : this(null, null, null, null)
 }

+ 15 - 0
app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java

@@ -6,6 +6,7 @@
  */
 package com.nextcloud.talk.signaling;
 
+import com.nextcloud.talk.models.json.converters.EnumActorTypeConverter;
 import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter;
 import com.nextcloud.talk.models.json.participants.Participant;
 import com.nextcloud.talk.models.json.signaling.NCIceCandidate;
@@ -38,6 +39,8 @@ import java.util.Map;
  */
 public abstract class SignalingMessageReceiver {
 
+    private final EnumActorTypeConverter enumActorTypeConverter = new EnumActorTypeConverter();
+
     private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier();
 
     private final LocalParticipantMessageNotifier localParticipantMessageNotifier = new LocalParticipantMessageNotifier();
@@ -398,6 +401,8 @@ public abstract class SignalingMessageReceiver {
         //                     "nextcloudSessionId": #STRING#, // Optional
         //                     "internal": #BOOLEAN#, // Optional
         //                     "participantPermissions": #INTEGER#, // Talk >= 13
+        //                     "actorType": #STRING#, // Talk >= 20
+        //                     "actorId": #STRING#, // Talk >= 20
         //                 },
         //                 ...
         //             ],
@@ -447,6 +452,8 @@ public abstract class SignalingMessageReceiver {
         //             "sessionId": #STRING#,
         //             "userId": #STRING#, // Always included, although it can be empty
         //             "participantPermissions": #INTEGER#, // Talk >= 13
+        //             "actorType": #STRING#, // Talk >= 20
+        //             "actorId": #STRING#, // Talk >= 20
         //         },
         //         ...
         //     ],
@@ -492,6 +499,14 @@ public abstract class SignalingMessageReceiver {
             participant.setInternal(Boolean.TRUE);
         }
 
+        if (participantMap.get("actorType") != null && !participantMap.get("actorType").toString().isEmpty()) {
+            participant.setActorType(enumActorTypeConverter.getFromString(participantMap.get("actorType").toString()));
+        }
+
+        if (participantMap.get("actorId") != null && !participantMap.get("actorId").toString().isEmpty()) {
+            participant.setActorId(participantMap.get("actorId").toString());
+        }
+
         // Only in external signaling messages
         if (participantMap.get("participantType") != null) {
             int participantTypeInt = Integer.parseInt(participantMap.get("participantType").toString());

+ 4 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.kt

@@ -283,6 +283,10 @@ object ApiUtils {
         return getUrlForSignaling(version, baseUrl) + "/settings"
     }
 
+    fun getUrlForSignalingSettings(version: Int, baseUrl: String?, token: String): String {
+        return getUrlForSignaling(version, baseUrl) + "/settings?token=" + token
+    }
+
     fun getUrlForSignaling(version: Int, baseUrl: String?, token: String): String {
         return getUrlForSignaling(version, baseUrl) + "/" + token
     }

+ 16 - 1
app/src/main/java/com/nextcloud/talk/webrtc/WebSocketConnectionHelper.java

@@ -12,6 +12,7 @@ import android.util.Log;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
+import com.nextcloud.talk.models.json.signaling.settings.FederationSettings;
 import com.nextcloud.talk.models.json.websocket.ActorWebSocketMessage;
 import com.nextcloud.talk.models.json.websocket.AuthParametersWebSocketMessage;
 import com.nextcloud.talk.models.json.websocket.AuthWebSocketMessage;
@@ -19,6 +20,7 @@ import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage;
 import com.nextcloud.talk.models.json.websocket.CallWebSocketMessage;
 import com.nextcloud.talk.models.json.websocket.HelloOverallWebSocketMessage;
 import com.nextcloud.talk.models.json.websocket.HelloWebSocketMessage;
+import com.nextcloud.talk.models.json.websocket.RoomFederationWebSocketMessage;
 import com.nextcloud.talk.models.json.websocket.RoomOverallWebSocketMessage;
 import com.nextcloud.talk.models.json.websocket.RoomWebSocketMessage;
 import com.nextcloud.talk.utils.ApiUtils;
@@ -128,12 +130,25 @@ public class WebSocketConnectionHelper {
         return helloOverallWebSocketMessage;
     }
 
-    RoomOverallWebSocketMessage getAssembledJoinOrLeaveRoomModel(String roomId, String sessionId) {
+    RoomOverallWebSocketMessage getAssembledJoinOrLeaveRoomModel(String roomId, String sessionId,
+                                                                 FederationSettings federation) {
         RoomOverallWebSocketMessage roomOverallWebSocketMessage = new RoomOverallWebSocketMessage();
         roomOverallWebSocketMessage.setType("room");
         RoomWebSocketMessage roomWebSocketMessage = new RoomWebSocketMessage();
         roomWebSocketMessage.setRoomId(roomId);
         roomWebSocketMessage.setSessionId(sessionId);
+        if (federation != null) {
+            String federationAuthToken = null;
+            if (federation.getHelloAuthParams() != null) {
+                federationAuthToken = federation.getHelloAuthParams().getToken();
+            }
+            RoomFederationWebSocketMessage roomFederationWebSocketMessage = new RoomFederationWebSocketMessage();
+            roomFederationWebSocketMessage.setSignaling(federation.getServer());
+            roomFederationWebSocketMessage.setUrl(federation.getNextcloudServer() + "/ocs/v2.php/apps/spreed/api/v3/signaling/backend");
+            roomFederationWebSocketMessage.setRoomid(federation.getRoomId());
+            roomFederationWebSocketMessage.setToken(federationAuthToken);
+            roomWebSocketMessage.setRoomFederationWebSocketMessage(roomFederationWebSocketMessage);
+        }
         roomOverallWebSocketMessage.setRoomWebSocketMessage(roomWebSocketMessage);
         return roomOverallWebSocketMessage;
     }

+ 17 - 3
app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt

@@ -20,6 +20,7 @@ import com.nextcloud.talk.events.WebSocketCommunicationEvent
 import com.nextcloud.talk.models.json.participants.Participant
 import com.nextcloud.talk.models.json.participants.Participant.ActorType
 import com.nextcloud.talk.models.json.signaling.NCSignalingMessage
+import com.nextcloud.talk.models.json.signaling.settings.FederationSettings
 import com.nextcloud.talk.models.json.websocket.BaseWebSocketMessage
 import com.nextcloud.talk.models.json.websocket.ByeWebSocketMessage
 import com.nextcloud.talk.models.json.websocket.CallOverallWebSocketMessage
@@ -75,6 +76,7 @@ class WebSocketInstance internal constructor(
     private val connectionUrl: String
     private var currentRoomToken: String? = null
     private var currentNormalBackendSession: String? = null
+    private var currentFederation: FederationSettings? = null
     private var reconnecting = false
     private val usersHashMap: HashMap<String?, Participant>
     private var messagesQueue: MutableList<String> = ArrayList()
@@ -367,24 +369,36 @@ class WebSocketInstance internal constructor(
         return hasMCU
     }
 
-    fun joinRoomWithRoomTokenAndSession(roomToken: String, normalBackendSession: String?) {
+    @Suppress("Detekt.ComplexMethod")
+    fun joinRoomWithRoomTokenAndSession(
+        roomToken: String,
+        normalBackendSession: String?,
+        federation: FederationSettings? = null
+    ) {
         Log.d(TAG, "joinRoomWithRoomTokenAndSession")
         Log.d(TAG, "   roomToken: $roomToken")
         Log.d(TAG, "   session: $normalBackendSession")
         try {
             val message = LoganSquare.serialize(
-                webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession)
+                webSocketConnectionHelper.getAssembledJoinOrLeaveRoomModel(roomToken, normalBackendSession, federation)
             )
             if (roomToken == "") {
                 Log.d(TAG, "sending 'leave room' via websocket")
                 currentNormalBackendSession = ""
+                currentFederation = null
                 sendMessage(message)
-            } else if (roomToken == currentRoomToken && normalBackendSession == currentNormalBackendSession) {
+            } else if (
+                roomToken == currentRoomToken &&
+                normalBackendSession == currentNormalBackendSession &&
+                federation?.roomId == currentFederation?.roomId &&
+                federation?.nextcloudServer == federation?.nextcloudServer
+            ) {
                 Log.d(TAG, "roomToken & session are unchanged. Joining locally without to send websocket message")
                 sendRoomJoinedEvent()
             } else {
                 Log.d(TAG, "Sending join room message via websocket")
                 currentNormalBackendSession = normalBackendSession
+                currentFederation = federation
                 sendMessage(message)
             }
         } catch (e: IOException) {

+ 4 - 0
app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java

@@ -71,6 +71,8 @@ public class CallParticipantListExternalSignalingTest {
             participant.setSessionId(sessionId);
             participant.setType(type);
             participant.setUserId(userId);
+            participant.setActorType(Participant.ActorType.USERS);
+            participant.setActorId(userId);
 
             return participant;
         }
@@ -81,6 +83,8 @@ public class CallParticipantListExternalSignalingTest {
             participant.setLastPing(lastPing);
             participant.setSessionId(sessionId);
             participant.setType(type);
+            participant.setActorType(Participant.ActorType.GUESTS);
+            participant.setActorId("sha1-" + sessionId);
 
             return participant;
         }

+ 4 - 0
app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java

@@ -61,6 +61,8 @@ public class CallParticipantListInternalSignalingTest {
             participant.setLastPing(lastPing);
             participant.setSessionId(sessionId);
             participant.setUserId(userId);
+            participant.setActorType(Participant.ActorType.USERS);
+            participant.setActorId(userId);
 
             return participant;
         }
@@ -70,6 +72,8 @@ public class CallParticipantListInternalSignalingTest {
             participant.setInCall(inCall);
             participant.setLastPing(lastPing);
             participant.setSessionId(sessionId);
+            participant.setActorType(Participant.ActorType.GUESTS);
+            participant.setActorId("sha1-" + sessionId);
 
             return participant;
         }

+ 14 - 6
app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java

@@ -58,10 +58,12 @@ public class SignalingMessageReceiverParticipantListTest {
         user1.put("roomId", 108);
         user1.put("sessionId", "theSessionId1");
         user1.put("userId", "theUserId");
-        // If "participantPermissions" is set in any of the participants all the other participants in the message
-        // would have it too. But for test simplicity, and as it is not relevant for the processing, in this test it
-        // is included only in one of the participants.
+        // If any of the following properties is set in any of the participants all the other participants in the
+        // message would have it too. But for test simplicity, and as it is not relevant for the processing, in this
+        // test they are included only in one of the participants.
         user1.put("participantPermissions", 42);
+        user1.put("actorType", "federated_users");
+        user1.put("actorId", "theActorId");
         users.add(user1);
         Map<String, Object> user2 = new HashMap<>();
         user2.put("inCall", 0);
@@ -78,6 +80,8 @@ public class SignalingMessageReceiverParticipantListTest {
         expectedParticipant1.setLastPing(4815);
         expectedParticipant1.setSessionId("theSessionId1");
         expectedParticipant1.setUserId("theUserId");
+        expectedParticipant1.setActorType(Participant.ActorType.FEDERATED);
+        expectedParticipant1.setActorId("theActorId");
         expectedParticipantList.add(expectedParticipant1);
 
         Participant expectedParticipant2 = new Participant();
@@ -266,11 +270,13 @@ public class SignalingMessageReceiverParticipantListTest {
         user1.put("sessionId", "theSessionId1");
         user1.put("participantType", 3);
         user1.put("userId", "theUserId");
-        // If "nextcloudSessionId" or "participantPermissions" is set in any of the participants all the other
-        // participants in the message would have them too. But for test simplicity, and as it is not relevant for
-        // the processing, in this test they are included only in one of the participants.
+        // If any of the following properties is set in any of the participants all the other participants in the
+        // message would have it too. But for test simplicity, and as it is not relevant for the processing, in this
+        // test they are included only in one of the participants.
         user1.put("nextcloudSessionId", "theNextcloudSessionId");
         user1.put("participantPermissions", 42);
+        user1.put("actorType", "federated_users");
+        user1.put("actorId", "theActorId");
         users.add(user1);
         Map<String, Object> user2 = new HashMap<>();
         user2.put("inCall", 0);
@@ -289,6 +295,8 @@ public class SignalingMessageReceiverParticipantListTest {
         expectedParticipant1.setSessionId("theSessionId1");
         expectedParticipant1.setType(Participant.ParticipantType.USER);
         expectedParticipant1.setUserId("theUserId");
+        expectedParticipant1.setActorType(Participant.ActorType.FEDERATED);
+        expectedParticipant1.setActorId("theActorId");
         expectedParticipantList.add(expectedParticipant1);
 
         Participant expectedParticipant2 = new Participant();