Browse Source

Add listener for call participant messages

Although "unshareScreen" is technically bound to a specific peer
connection it is instead treated as a general message on the call
participant.

Nevertheless, call participant messages will make possible (at a later
point) to listen to events like "raise hand" or "mute" (which, again,
could be technically bound to a specific peer connection, but at least
for now are treated as a general message on the call participant).

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
Daniel Calviño Sánchez 2 năm trước cách đây
mục cha
commit
bda7d2719b

+ 31 - 8
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -266,6 +266,9 @@ public class CallActivity extends CallBaseActivity {
 
     private CallActivitySignalingMessageReceiver signalingMessageReceiver = new CallActivitySignalingMessageReceiver();
 
+    private Map<String, SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners =
+        new HashMap<>();
+
     private SignalingMessageReceiver.OfferMessageListener offerMessageListener = new SignalingMessageReceiver.OfferMessageListener() {
         @Override
         public void onOffer(String sessionId, String roomType, String sdp, String nick) {
@@ -1675,14 +1678,6 @@ public class CallActivity extends CallBaseActivity {
 
     private void processMessage(NCSignalingMessage ncSignalingMessage) {
         if ("video".equals(ncSignalingMessage.getRoomType()) || "screen".equals(ncSignalingMessage.getRoomType())) {
-            String type = ncSignalingMessage.getType();
-
-            if ("unshareScreen".equals(type)) {
-                endPeerConnection(ncSignalingMessage.getFrom(), true);
-
-                return;
-            }
-
             signalingMessageReceiver.process(ncSignalingMessage);
         } else {
             Log.e(TAG, "unexpected RoomType while processing NCSignalingMessage");
@@ -2028,6 +2023,15 @@ public class CallActivity extends CallBaseActivity {
 
             peerConnectionWrapperList.add(peerConnectionWrapper);
 
+            // Currently there is no separation between call participants and peer connections, so any video peer
+            // connection (except the own publisher connection) is treated as a call participant.
+            if (!publisher && "video".equals(type)) {
+                SignalingMessageReceiver.CallParticipantMessageListener callParticipantMessageListener =
+                    new CallActivityCallParticipantMessageListener(sessionId);
+                callParticipantMessageListeners.put(sessionId, callParticipantMessageListener);
+                signalingMessageReceiver.addListener(callParticipantMessageListener, sessionId);
+            }
+
             if (publisher) {
                 startSendingNick();
             }
@@ -2060,6 +2064,11 @@ public class CallActivity extends CallBaseActivity {
                 }
             }
         }
+
+        if (!justScreen) {
+            SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
+            signalingMessageReceiver.removeListener(listener);
+        }
     }
 
     private void removeMediaStream(String sessionId, String videoStreamType) {
@@ -2642,6 +2651,20 @@ public class CallActivity extends CallBaseActivity {
         }
     }
 
+    private class CallActivityCallParticipantMessageListener implements SignalingMessageReceiver.CallParticipantMessageListener {
+
+        private final String sessionId;
+
+        public CallActivityCallParticipantMessageListener(String sessionId) {
+            this.sessionId = sessionId;
+        }
+
+        @Override
+        public void onUnshareScreen() {
+            endPeerConnection(sessionId, true);
+        }
+    }
+
     private class MicrophoneButtonTouchListener implements View.OnTouchListener {
 
         @SuppressLint("ClickableViewAccessibility")

+ 95 - 0
app/src/main/java/com/nextcloud/talk/signaling/CallParticipantMessageNotifier.java

@@ -0,0 +1,95 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Daniel Calviño Sánchez
+ * Copyright (C) 2022 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.Iterator;
+import java.util.List;
+
+/**
+ * Helper class to register and notify CallParticipantMessageListeners.
+ *
+ * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against
+ * a SignalingMessageReceiver rather than against a CallParticipantMessageNotifier.
+ */
+class CallParticipantMessageNotifier {
+
+    /**
+     * Helper class to associate a CallParticipantMessageListener with a session ID.
+     */
+    private static class CallParticipantMessageListenerFrom {
+        public final SignalingMessageReceiver.CallParticipantMessageListener listener;
+        public final String sessionId;
+
+        private CallParticipantMessageListenerFrom(SignalingMessageReceiver.CallParticipantMessageListener listener,
+                                                   String sessionId) {
+            this.listener = listener;
+            this.sessionId = sessionId;
+        }
+    }
+
+    private final List<CallParticipantMessageListenerFrom> callParticipantMessageListenersFrom = new ArrayList<>();
+
+    public synchronized void addListener(SignalingMessageReceiver.CallParticipantMessageListener listener, String sessionId) {
+        if (listener == null) {
+            throw new IllegalArgumentException("CallParticipantMessageListener can not be null");
+        }
+
+        if (sessionId == null) {
+            throw new IllegalArgumentException("sessionId can not be null");
+        }
+
+        removeListener(listener);
+
+        callParticipantMessageListenersFrom.add(new CallParticipantMessageListenerFrom(listener, sessionId));
+    }
+
+    public synchronized void removeListener(SignalingMessageReceiver.CallParticipantMessageListener listener) {
+        Iterator<CallParticipantMessageListenerFrom> it = callParticipantMessageListenersFrom.iterator();
+        while (it.hasNext()) {
+            CallParticipantMessageListenerFrom listenerFrom = it.next();
+
+            if (listenerFrom.listener == listener) {
+                it.remove();
+
+                return;
+            }
+        }
+    }
+
+    private List<SignalingMessageReceiver.CallParticipantMessageListener> getListenersFor(String sessionId) {
+        List<SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners =
+            new ArrayList<>(callParticipantMessageListenersFrom.size());
+
+        for (CallParticipantMessageListenerFrom listenerFrom : callParticipantMessageListenersFrom) {
+            if (listenerFrom.sessionId.equals(sessionId)) {
+                callParticipantMessageListeners.add(listenerFrom.listener);
+            }
+        }
+
+        return callParticipantMessageListeners;
+    }
+
+    public synchronized void notifyUnshareScreen(String sessionId) {
+        for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) {
+            listener.onUnshareScreen();
+        }
+    }
+}

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

@@ -44,6 +44,19 @@ import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
  */
 public abstract class SignalingMessageReceiver {
 
+    /**
+     * Listener for call participant messages.
+     *
+     * The messages are bound to a specific call participant (or, rather, session), so each listener is expected to
+     * handle messages only for a single call participant.
+     *
+     * Although "unshareScreen" is technically bound to a specific peer connection it is instead treated as a general
+     * message on the call participant.
+     */
+    public interface CallParticipantMessageListener {
+        void onUnshareScreen();
+    }
+
     /**
      * Listener for WebRTC offers.
      *
@@ -70,10 +83,29 @@ public abstract class SignalingMessageReceiver {
         void onEndOfCandidates();
     }
 
+    private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier();
+
     private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier();
 
     private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier();
 
+    /**
+     * Adds a listener for call participant messages.
+     *
+     * A listener is expected to be added only once. If the same listener is added again it will no longer be notified
+     * for the messages from the previous session ID.
+     *
+     * @param listener the CallParticipantMessageListener
+     * @param sessionId the ID of the session that messages come from
+     */
+    public void addListener(CallParticipantMessageListener listener, String sessionId) {
+        callParticipantMessageNotifier.addListener(listener, sessionId);
+    }
+
+    public void removeListener(CallParticipantMessageListener listener) {
+        callParticipantMessageNotifier.removeListener(listener);
+    }
+
     /**
      * Adds a listener for all offer messages.
      *
@@ -116,6 +148,43 @@ public abstract class SignalingMessageReceiver {
         String sessionId = signalingMessage.getFrom();
         String roomType = signalingMessage.getRoomType();
 
+        // "unshareScreen" messages are directly sent to the screen peer connection when the internal signaling
+        // server is used, and to the room when the external signaling server is used. However, the (relevant) data
+        // of the received message ("from" and "type") is the same in both cases.
+        if ("unshareScreen".equals(type)) {
+            // Message schema (external signaling server):
+            // {
+            //     "type": "message",
+            //     "message": {
+            //         "sender": {
+            //             ...
+            //         },
+            //         "data": {
+            //             "roomType": "screen",
+            //             "type": "unshareScreen",
+            //             "from": #STRING#,
+            //         },
+            //     },
+            // }
+            //
+            // Message schema (internal signaling server):
+            // {
+            //     "type": "message",
+            //     "data": {
+            //         "to": #STRING#,
+            //         "sid": #STRING#,
+            //         "broadcaster": #STRING#,
+            //         "roomType": "screen",
+            //         "type": "unshareScreen",
+            //         "from": #STRING#,
+            //     },
+            // }
+
+            callParticipantMessageNotifier.notifyUnshareScreen(sessionId);
+
+            return;
+        }
+
         if ("offer".equals(type)) {
             // Message schema (external signaling server):
             // {

+ 233 - 0
app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverCallParticipantTest.java

@@ -0,0 +1,233 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Daniel Calviño Sánchez
+ * Copyright (C) 2022 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 com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+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 SignalingMessageReceiverCallParticipantTest {
+
+    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 testAddCallParticipantMessageListenerWithNullListener() {
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            signalingMessageReceiver.addListener(null, "theSessionId");
+        });
+    }
+
+    @Test
+    public void testAddCallParticipantMessageListenerWithNullSessionId() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        Assert.assertThrows(IllegalArgumentException.class, () -> {
+            signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, null);
+        });
+    }
+
+    @Test
+    public void testCallParticipantMessageUnshareScreen() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedCallParticipantMessageListener, only()).onUnshareScreen();
+    }
+
+    @Test
+    public void testCallParticipantMessageSeveralListenersSameFrom() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId");
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen();
+        verify(mockedCallParticipantMessageListener2, only()).onUnshareScreen();
+    }
+
+    @Test
+    public void testCallParticipantMessageNotMatchingSessionId() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("notMatchingSessionId");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verifyNoInteractions(mockedCallParticipantMessageListener);
+    }
+
+    @Test
+    public void testCallParticipantMessageAfterRemovingListener() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
+        signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verifyNoInteractions(mockedCallParticipantMessageListener);
+    }
+
+    @Test
+    public void testCallParticipantMessageAfterRemovingSingleListenerOfSeveral() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener3 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId");
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId");
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener3, "theSessionId");
+        signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener2);
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen();
+        verify(mockedCallParticipantMessageListener3, only()).onUnshareScreen();
+        verifyNoInteractions(mockedCallParticipantMessageListener2);
+    }
+
+    @Test
+    public void testCallParticipantMessageAfterAddingListenerAgainForDifferentFrom() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId2");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verifyNoInteractions(mockedCallParticipantMessageListener);
+
+        signalingMessage.setFrom("theSessionId2");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedCallParticipantMessageListener, only()).onUnshareScreen();
+    }
+
+    @Test
+    public void testAddCallParticipantMessageListenerWhenHandlingCallParticipantMessage() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        doAnswer((invocation) -> {
+            signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId");
+            return null;
+        }).when(mockedCallParticipantMessageListener1).onUnshareScreen();
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedCallParticipantMessageListener1, only()).onUnshareScreen();
+        verifyNoInteractions(mockedCallParticipantMessageListener2);
+    }
+
+    @Test
+    public void testRemoveCallParticipantMessageListenerWhenHandlingCallParticipantMessage() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener1 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener2 =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        doAnswer((invocation) -> {
+            signalingMessageReceiver.removeListener(mockedCallParticipantMessageListener2);
+            return null;
+        }).when(mockedCallParticipantMessageListener1).onUnshareScreen();
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener1, "theSessionId");
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener2, "theSessionId");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("unshareScreen");
+        signalingMessage.setRoomType("theRoomType");
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        InOrder inOrder = inOrder(mockedCallParticipantMessageListener1, mockedCallParticipantMessageListener2);
+
+        inOrder.verify(mockedCallParticipantMessageListener1).onUnshareScreen();
+        inOrder.verify(mockedCallParticipantMessageListener2).onUnshareScreen();
+    }
+}