Przeglądaj źródła

Add listener for local participant signaling messages

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Daniel Calviño Sánchez 2 lat temu
rodzic
commit
747a4646d3

+ 53 - 0
app/src/main/java/com/nextcloud/talk/signaling/LocalParticipantMessageNotifier.java

@@ -0,0 +1,53 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Daniel Calviño Sánchez
+ * Copyright (C) 2023 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.signaling;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+/**
+ * Helper class to register and notify LocalParticipantMessageListeners.
+ *
+ * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against
+ * a SignalingMessageReceiver rather than against a LocalParticipantMessageNotifier.
+ */
+class LocalParticipantMessageNotifier {
+
+    private final Set<SignalingMessageReceiver.LocalParticipantMessageListener> localParticipantMessageListeners = new LinkedHashSet<>();
+
+    public synchronized void addListener(SignalingMessageReceiver.LocalParticipantMessageListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("localParticipantMessageListener can not be null");
+        }
+
+        localParticipantMessageListeners.add(listener);
+    }
+
+    public synchronized void removeListener(SignalingMessageReceiver.LocalParticipantMessageListener listener) {
+        localParticipantMessageListeners.remove(listener);
+    }
+
+    public synchronized void notifySwitchTo(String token) {
+        for (SignalingMessageReceiver.LocalParticipantMessageListener listener : new ArrayList<>(localParticipantMessageListeners)) {
+            listener.onSwitchTo(token);
+        }
+    }
+}

+ 78 - 2
app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java

@@ -118,6 +118,26 @@ public abstract class SignalingMessageReceiver {
         void onAllParticipantsUpdate(long inCall);
         void onAllParticipantsUpdate(long inCall);
     }
     }
 
 
+    /**
+     * Listener for local participant messages.
+     *
+     * The messages are implicitly bound to the local participant (or, rather, its session); listeners are expected
+     * to know the local participant.
+     *
+     * The messages are related to the conversation, so the local participant may or may not be in a call when they
+     * are received.
+     */
+    public interface LocalParticipantMessageListener {
+        /**
+         * Request for the client to switch to the given conversation.
+         *
+         * This message is received only when the external signaling server is used.
+         *
+         * @param token the token of the conversation to switch to.
+         */
+        void onSwitchTo(String token);
+    }
+
     /**
     /**
      * Listener for call participant messages.
      * Listener for call participant messages.
      *
      *
@@ -160,6 +180,8 @@ public abstract class SignalingMessageReceiver {
 
 
     private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier();
     private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier();
 
 
+    private final LocalParticipantMessageNotifier localParticipantMessageNotifier = new LocalParticipantMessageNotifier();
+
     private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier();
     private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier();
 
 
     private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier();
     private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier();
@@ -181,6 +203,21 @@ public abstract class SignalingMessageReceiver {
         participantListMessageNotifier.removeListener(listener);
         participantListMessageNotifier.removeListener(listener);
     }
     }
 
 
+    /**
+     * Adds a listener for local participant messages.
+     *
+     * A listener is expected to be added only once. If the same listener is added again it will be notified just once.
+     *
+     * @param listener the LocalParticipantMessageListener
+     */
+    public void addListener(LocalParticipantMessageListener listener) {
+        localParticipantMessageNotifier.addListener(listener);
+    }
+
+    public void removeListener(LocalParticipantMessageListener listener) {
+        localParticipantMessageNotifier.removeListener(listener);
+    }
+
     /**
     /**
      * Adds a listener for call participant messages.
      * Adds a listener for call participant messages.
      *
      *
@@ -232,17 +269,56 @@ public abstract class SignalingMessageReceiver {
     }
     }
 
 
     protected void processEvent(Map<String, Object> eventMap) {
     protected void processEvent(Map<String, Object> eventMap) {
-        if (!"participants".equals(eventMap.get("target"))) {
+        if ("room".equals(eventMap.get("target")) && "switchto".equals(eventMap.get("type"))) {
+            processSwitchToEvent(eventMap);
+
             return;
             return;
         }
         }
 
 
-        if ("update".equals(eventMap.get("type"))) {
+        if ("participants".equals(eventMap.get("target")) && "update".equals(eventMap.get("type"))) {
             processUpdateEvent(eventMap);
             processUpdateEvent(eventMap);
 
 
             return;
             return;
         }
         }
     }
     }
 
 
+    private void processSwitchToEvent(Map<String, Object> eventMap) {
+        // Message schema:
+        // {
+        //     "type": "event",
+        //     "event": {
+        //         "target": "room",
+        //         "type": "switchto",
+        //         "switchto": {
+        //             "roomid": #STRING#,
+        //         },
+        //     },
+        // }
+
+        Map<String, Object> switchToMap;
+        try {
+            switchToMap = (Map<String, Object>) eventMap.get("switchto");
+        } catch (RuntimeException e) {
+            // Broken message, this should not happen.
+            return;
+        }
+
+        if (switchToMap == null) {
+            // Broken message, this should not happen.
+            return;
+        }
+
+        String token;
+        try {
+            token = switchToMap.get("roomid").toString();
+        } catch (RuntimeException e) {
+            // Broken message, this should not happen.
+            return;
+        }
+
+        localParticipantMessageNotifier.notifySwitchTo(token);
+    }
+
     private void processUpdateEvent(Map<String, Object> eventMap) {
     private void processUpdateEvent(Map<String, Object> eventMap) {
         Map<String, Object> updateMap;
         Map<String, Object> updateMap;
         try {
         try {

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

@@ -201,12 +201,14 @@ class WebSocketInstance internal constructor(
             val target = eventOverallWebSocketMessage.eventMap!!["target"] as String?
             val target = eventOverallWebSocketMessage.eventMap!!["target"] as String?
             if (target != null) {
             if (target != null) {
                 when (target) {
                 when (target) {
-                    Globals.TARGET_ROOM ->
+                    Globals.TARGET_ROOM -> {
                         if ("message" == eventOverallWebSocketMessage.eventMap!!["type"]) {
                         if ("message" == eventOverallWebSocketMessage.eventMap!!["type"]) {
                             processRoomMessageMessage(eventOverallWebSocketMessage)
                             processRoomMessageMessage(eventOverallWebSocketMessage)
                         } else if ("join" == eventOverallWebSocketMessage.eventMap!!["type"]) {
                         } else if ("join" == eventOverallWebSocketMessage.eventMap!!["type"]) {
                             processRoomJoinMessage(eventOverallWebSocketMessage)
                             processRoomJoinMessage(eventOverallWebSocketMessage)
                         }
                         }
+                        signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap)
+                    }
                     Globals.TARGET_PARTICIPANTS ->
                     Globals.TARGET_PARTICIPANTS ->
                         signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap)
                         signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap)
                     else ->
                     else ->

+ 193 - 0
app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverLocalParticipantTest.java

@@ -0,0 +1,193 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Daniel Calviño Sánchez
+ * Copyright (C) 2023 Daniel Calviño Sánchez <danxuliu@gmail.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.signaling;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.only;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+public class SignalingMessageReceiverLocalParticipantTest {
+
+    private SignalingMessageReceiver signalingMessageReceiver;
+
+    @Before
+    public void setUp() {
+        // SignalingMessageReceiver is abstract to prevent direct instantiation without calling the appropriate
+        // protected methods.
+        signalingMessageReceiver = new SignalingMessageReceiver() {
+        };
+    }
+
+    @Test
+    public void testAddLocalParticipantMessageListenerWithNullListener() {
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            signalingMessageReceiver.addListener((SignalingMessageReceiver.LocalParticipantMessageListener) null);
+        });
+    }
+
+    @Test
+    public void testExternalSignalingLocalParticipantMessageSwitchTo() {
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener);
+
+        Map<String, Object> eventMap = new HashMap<>();
+        eventMap.put("type", "switchto");
+        eventMap.put("target", "room");
+        Map<String, Object> switchToMap = new HashMap<>();
+        switchToMap.put("roomid", "theToken");
+        eventMap.put("switchto", switchToMap);
+        signalingMessageReceiver.processEvent(eventMap);
+
+        verify(mockedLocalParticipantMessageListener, only()).onSwitchTo("theToken");
+    }
+
+    @Test
+    public void testExternalSignalingLocalParticipantMessageAfterRemovingListener() {
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener);
+        signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener);
+
+        Map<String, Object> eventMap = new HashMap<>();
+        eventMap.put("type", "switchto");
+        eventMap.put("target", "room");
+        HashMap<String, Object> switchToMap = new HashMap<>();
+        switchToMap.put("roomid", "theToken");
+        eventMap.put("switchto", switchToMap);
+        signalingMessageReceiver.processEvent(eventMap);
+
+        verifyNoInteractions(mockedLocalParticipantMessageListener);
+    }
+
+    @Test
+    public void testExternalSignalingLocalParticipantMessageAfterRemovingSingleListenerOfSeveral() {
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener3 =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1);
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2);
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener3);
+        signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener2);
+
+        Map<String, Object> eventMap = new HashMap<>();
+        eventMap.put("type", "switchto");
+        eventMap.put("target", "room");
+        HashMap<String, Object> switchToMap = new HashMap<>();
+        switchToMap.put("roomid", "theToken");
+        eventMap.put("switchto", switchToMap);
+        signalingMessageReceiver.processEvent(eventMap);
+
+        verify(mockedLocalParticipantMessageListener1, only()).onSwitchTo("theToken");
+        verify(mockedLocalParticipantMessageListener3, only()).onSwitchTo("theToken");
+        verifyNoInteractions(mockedLocalParticipantMessageListener2);
+    }
+
+    @Test
+    public void testExternalSignalingLocalParticipantMessageAfterAddingListenerAgain() {
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener);
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener);
+
+        Map<String, Object> eventMap = new HashMap<>();
+        eventMap.put("type", "switchto");
+        eventMap.put("target", "room");
+        HashMap<String, Object> switchToMap = new HashMap<>();
+        switchToMap.put("roomid", "theToken");
+        eventMap.put("switchto", switchToMap);
+        signalingMessageReceiver.processEvent(eventMap);
+
+        verify(mockedLocalParticipantMessageListener, only()).onSwitchTo("theToken");
+    }
+
+    @Test
+    public void testAddLocalParticipantMessageListenerWhenHandlingExternalSignalingLocalParticipantMessage() {
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+
+        doAnswer((invocation) -> {
+            signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2);
+            return null;
+        }).when(mockedLocalParticipantMessageListener1).onSwitchTo("theToken");
+
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1);
+
+        Map<String, Object> eventMap = new HashMap<>();
+        eventMap.put("type", "switchto");
+        eventMap.put("target", "room");
+        HashMap<String, Object> switchToMap = new HashMap<>();
+        switchToMap.put("roomid", "theToken");
+        eventMap.put("switchto", switchToMap);
+        signalingMessageReceiver.processEvent(eventMap);
+
+        verify(mockedLocalParticipantMessageListener1, only()).onSwitchTo("theToken");
+        verifyNoInteractions(mockedLocalParticipantMessageListener2);
+    }
+
+    @Test
+    public void testRemoveLocalParticipantMessageListenerWhenHandlingExternalSignalingLocalParticipantMessage() {
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+        SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 =
+            mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
+
+        doAnswer((invocation) -> {
+            signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener2);
+            return null;
+        }).when(mockedLocalParticipantMessageListener1).onSwitchTo("theToken");
+
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1);
+        signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2);
+
+        Map<String, Object> eventMap = new HashMap<>();
+        eventMap.put("type", "switchto");
+        eventMap.put("target", "room");
+        HashMap<String, Object> switchToMap = new HashMap<>();
+        switchToMap.put("roomid", "theToken");
+        eventMap.put("switchto", switchToMap);
+        signalingMessageReceiver.processEvent(eventMap);
+
+        InOrder inOrder = inOrder(mockedLocalParticipantMessageListener1, mockedLocalParticipantMessageListener2);
+
+        inOrder.verify(mockedLocalParticipantMessageListener1).onSwitchTo("theToken");
+        inOrder.verify(mockedLocalParticipantMessageListener2).onSwitchTo("theToken");
+    }
+}