diff --git a/app/src/main/java/com/nextcloud/talk/signaling/ParticipantListMessageNotifier.java b/app/src/main/java/com/nextcloud/talk/signaling/ParticipantListMessageNotifier.java new file mode 100644 index 000000000..b37c3f659 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/ParticipantListMessageNotifier.java @@ -0,0 +1,68 @@ +/* + * Nextcloud Talk application + * + * @author Daniel Calviño Sánchez + * Copyright (C) 2022 Daniel Calviño Sánchez + * + * 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 . + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.participants.Participant; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Helper class to register and notify ParticipantListMessageListeners. + * + * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against + * a SignalingMessageReceiver rather than against a ParticipantListMessageNotifier. + */ +class ParticipantListMessageNotifier { + + private final Set participantListMessageListeners = new LinkedHashSet<>(); + + public synchronized void addListener(SignalingMessageReceiver.ParticipantListMessageListener listener) { + if (listener == null) { + throw new IllegalArgumentException("participantListMessageListeners can not be null"); + } + + participantListMessageListeners.add(listener); + } + + public synchronized void removeListener(SignalingMessageReceiver.ParticipantListMessageListener listener) { + participantListMessageListeners.remove(listener); + } + + public synchronized void notifyUsersInRoom(List participants) { + for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) { + listener.onUsersInRoom(participants); + } + } + + public synchronized void notifyParticipantsUpdate(List participants) { + for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) { + listener.onParticipantsUpdate(participants); + } + } + + public synchronized void notifyAllParticipantsUpdate(long inCall) { + for (SignalingMessageReceiver.ParticipantListMessageListener listener : new ArrayList<>(participantListMessageListeners)) { + listener.onAllParticipantsUpdate(inCall); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java index 6d1a90d45..161dae555 100644 --- a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java +++ b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java @@ -19,10 +19,16 @@ */ package com.nextcloud.talk.signaling; +import com.nextcloud.talk.models.json.converters.EnumParticipantTypeConverter; +import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.models.json.signaling.NCIceCandidate; import com.nextcloud.talk.models.json.signaling.NCMessagePayload; import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + /** * Hub to register listeners for signaling messages of different kinds. * @@ -44,6 +50,74 @@ import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; */ public abstract class SignalingMessageReceiver { + /** + * Listener for participant list messages. + * + * The messages are implicitly bound to the room currently joined in the signaling server; listeners are expected + * to know the current room. + */ + public interface ParticipantListMessageListener { + + /** + * List of all the participants in the room. + * + * This message is received only when the internal signaling server is used. + * + * The message is received periodically, and the participants may not have been modified since the last message. + * + * Only the following participant properties are set: + * - inCall + * - lastPing + * - sessionId + * - userId (if the participant is not a guest) + * + * "participantPermissions" is provided in the message (since Talk 13), but not currently set in the + * participant. "publishingPermissions" was provided instead in Talk 12, but it was not used anywhere, so it is + * ignored. + * + * @param participants all the participants (users and guests) in the room + */ + void onUsersInRoom(List participants); + + /** + * List of all the participants in the call or the room (depending on what triggered the event). + * + * This message is received only when the external signaling server is used. + * + * The message is received when any participant changed, although what changed is not provided and should be + * derived from the difference with previous messages. The list of participants may include only the + * participants in the call (including those that just left it and thus triggered the event) or all the + * participants currently in the room (participants in the room but not currently active, that is, without a + * session, are not included). + * + * Only the following participant properties are set: + * - inCall + * - lastPing + * - sessionId + * - type + * - userId (if the participant is not a guest) + * + * "nextcloudSessionId" is provided in the message (when the "inCall" property of any participant changed), but + * not currently set in the participant. + * + * "participantPermissions" is provided in the message (since Talk 13), but not currently set in the + * participant. "publishingPermissions" was provided instead in Talk 12, but it was not used anywhere, so it is + * ignored. + * + * @param participants all the participants (users and guests) in the room + */ + void onParticipantsUpdate(List participants); + + /** + * Update of the properties of all the participants in the room. + * + * This message is received only when the external signaling server is used. + * + * @param inCall the new value of the inCall property + */ + void onAllParticipantsUpdate(long inCall); + } + /** * Listener for call participant messages. * @@ -83,12 +157,29 @@ public abstract class SignalingMessageReceiver { void onEndOfCandidates(); } + private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier(); + private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier(); private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier(); private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier(); + /** + * Adds a listener for participant list 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 ParticipantListMessageListener + */ + public void addListener(ParticipantListMessageListener listener) { + participantListMessageNotifier.addListener(listener); + } + + public void removeListener(ParticipantListMessageListener listener) { + participantListMessageNotifier.removeListener(listener); + } + /** * Adds a listener for call participant messages. * @@ -139,6 +230,182 @@ public abstract class SignalingMessageReceiver { webRtcMessageNotifier.removeListener(listener); } + protected void processEvent(Map eventMap) { + if (!"update".equals(eventMap.get("type")) || !"participants".equals(eventMap.get("target"))) { + return; + } + + Map updateMap; + try { + updateMap = (Map) eventMap.get("update"); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + + if (updateMap == null) { + // Broken message, this should not happen. + return; + } + + if (updateMap.get("all") != null && Boolean.parseBoolean(updateMap.get("all").toString())) { + processAllParticipantsUpdate(updateMap); + + return; + } + + if (updateMap.get("users") != null) { + processParticipantsUpdate(updateMap); + + return; + } + } + + private void processAllParticipantsUpdate(Map updateMap) { + // Message schema: + // { + // "type": "event", + // "event": { + // "target": "participants", + // "type": "update", + // "update": { + // "roomid": #STRING#, + // "incall": 0, + // "all": true, + // }, + // }, + // } + + long inCall; + try { + inCall = Long.parseLong(updateMap.get("inCall").toString()); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + + participantListMessageNotifier.notifyAllParticipantsUpdate(inCall); + } + + private void processParticipantsUpdate(Map updateMap) { + // Message schema: + // { + // "type": "event", + // "event": { + // "target": "participants", + // "type": "update", + // "update": { + // "roomid": #INTEGER#, + // "users": [ + // { + // "inCall": #INTEGER#, + // "lastPing": #INTEGER#, + // "sessionId": #STRING#, + // "participantType": #INTEGER#, + // "userId": #STRING#, // Optional + // "nextcloudSessionId": #STRING#, // Optional + // "participantPermissions": #INTEGER#, // Talk >= 13 + // }, + // ... + // ], + // }, + // }, + // } + // + // Note that "userId" in participants->update comes from the Nextcloud server, so it is "userId"; in other + // messages, like room->join, it comes directly from the external signaling server, so it is "userid" instead. + + List> users; + try { + users = (List>) updateMap.get("users"); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + + if (users == null) { + // Broken message, this should not happen. + return; + } + + List participants = new ArrayList<>(users.size()); + + for (Map user: users) { + try { + participants.add(getParticipantFromMessageMap(user)); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + } + + participantListMessageNotifier.notifyParticipantsUpdate(participants); + } + + protected void processUsersInRoom(List> users) { + // Message schema: + // { + // "type": "usersInRoom", + // "data": [ + // { + // "inCall": #INTEGER#, + // "lastPing": #INTEGER#, + // "roomId": #INTEGER#, + // "sessionId": #STRING#, + // "userId": #STRING#, // Always included, although it can be empty + // "participantPermissions": #INTEGER#, // Talk >= 13 + // }, + // ... + // ], + // } + + List participants = new ArrayList<>(users.size()); + + for (Map user: users) { + try { + participants.add(getParticipantFromMessageMap(user)); + } catch (RuntimeException e) { + // Broken message, this should not happen. + return; + } + } + + participantListMessageNotifier.notifyUsersInRoom(participants); + } + + /** + * Creates and initializes a Participant from the data in the given map. + * + * Maps from internal and external signaling server messages can be used. Nevertheless, besides the differences + * between the messages and the optional properties, it is expected that the message is correct and the given data + * is parseable. Broken messages (for example, a string instead of an integer for "inCall" or a missing + * "sessionId") may cause a RuntimeException to be thrown. + * + * @param participantMap the map with the participant data + * @return the Participant + */ + private Participant getParticipantFromMessageMap(Map participantMap) { + Participant participant = new Participant(); + + participant.setInCall(Long.parseLong(participantMap.get("inCall").toString())); + participant.setLastPing(Long.parseLong(participantMap.get("lastPing").toString())); + participant.setSessionId(participantMap.get("sessionId").toString()); + + if (participantMap.get("userId") != null && !participantMap.get("userId").toString().isEmpty()) { + participant.setUserId(participantMap.get("userId").toString()); + } + + // Only in external signaling messages + if (participantMap.get("participantType") != null) { + int participantTypeInt = Integer.parseInt(participantMap.get("participantType").toString()); + + EnumParticipantTypeConverter converter = new EnumParticipantTypeConverter(); + participant.setType(converter.getFromInt(participantTypeInt)); + } + + return participant; + } + protected void processSignalingMessage(NCSignalingMessage signalingMessage) { // Note that in the internal signaling server message "data" is the String representation of a JSON // object, although it is already decoded when used here. diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java index 3491b5cbb..2163b6852 100644 --- a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverOfferTest.java @@ -49,7 +49,7 @@ public class SignalingMessageReceiverOfferTest { @Test public void testAddOfferMessageListenerWithNullListener() { Assert.assertThrows(IllegalArgumentException.class, () -> { - signalingMessageReceiver.addListener(null); + signalingMessageReceiver.addListener((SignalingMessageReceiver.OfferMessageListener) null); }); } diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java new file mode 100644 index 000000000..f3c965071 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverParticipantListTest.java @@ -0,0 +1,466 @@ +/* + * Nextcloud Talk application + * + * @author Daniel Calviño Sánchez + * Copyright (C) 2022 Daniel Calviño Sánchez + * + * 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 . + */ +package com.nextcloud.talk.signaling; + +import com.nextcloud.talk.models.json.participants.Participant; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +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 SignalingMessageReceiverParticipantListTest { + + 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 testAddParticipantListMessageListenerWithNullListener() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener((SignalingMessageReceiver.ParticipantListMessageListener) null); + }); + } + + @Test + public void testInternalSignalingParticipantListMessageUsersInRoom() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + List> users = new ArrayList<>(2); + Map user1 = new HashMap<>(); + user1.put("inCall", 7); + user1.put("lastPing", 4815); + 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. + user1.put("participantPermissions", 42); + users.add(user1); + Map user2 = new HashMap<>(); + user2.put("inCall", 0); + user2.put("lastPing", 162342); + user2.put("roomId", 108); + user2.put("sessionId", "theSessionId2"); + user2.put("userId", ""); + users.add(user2); + signalingMessageReceiver.processUsersInRoom(users); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant1 = new Participant(); + expectedParticipant1.setInCall(Participant.InCallFlags.IN_CALL | Participant.InCallFlags.WITH_AUDIO | Participant.InCallFlags.WITH_VIDEO); + expectedParticipant1.setLastPing(4815); + expectedParticipant1.setSessionId("theSessionId1"); + expectedParticipant1.setUserId("theUserId"); + expectedParticipantList.add(expectedParticipant1); + + Participant expectedParticipant2 = new Participant(); + expectedParticipant2.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant2.setLastPing(162342); + expectedParticipant2.setSessionId("theSessionId2"); + expectedParticipantList.add(expectedParticipant2); + + verify(mockedParticipantListMessageListener, only()).onUsersInRoom(expectedParticipantList); + } + + @Test + public void testInternalSignalingParticipantListMessageAfterRemovingListener() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + verifyNoInteractions(mockedParticipantListMessageListener); + } + + @Test + public void testInternalSignalingParticipantListMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener3 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener3); + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant = new Participant(); + expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant.setLastPing(4815); + expectedParticipant.setSessionId("theSessionId"); + expectedParticipantList.add(expectedParticipant); + + verify(mockedParticipantListMessageListener1, only()).onUsersInRoom(expectedParticipantList); + verify(mockedParticipantListMessageListener3, only()).onUsersInRoom(expectedParticipantList); + verifyNoInteractions(mockedParticipantListMessageListener2); + } + + @Test + public void testInternalSignalingParticipantListMessageAfterAddingListenerAgain() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant = new Participant(); + expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant.setLastPing(4815); + expectedParticipant.setSessionId("theSessionId"); + expectedParticipantList.add(expectedParticipant); + + verify(mockedParticipantListMessageListener, only()).onUsersInRoom(expectedParticipantList); + } + + @Test + public void testAddParticipantListMessageListenerWhenHandlingInternalSignalingParticipantListMessage() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant = new Participant(); + expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant.setLastPing(4815); + expectedParticipant.setSessionId("theSessionId"); + expectedParticipantList.add(expectedParticipant); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + return null; + }).when(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + verify(mockedParticipantListMessageListener1, only()).onUsersInRoom(expectedParticipantList); + verifyNoInteractions(mockedParticipantListMessageListener2); + } + + @Test + public void testRemoveParticipantListMessageListenerWhenHandlingInternalSignalingParticipantListMessage() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + List expectedParticipantList = new ArrayList<>(); + Participant expectedParticipant = new Participant(); + expectedParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant.setLastPing(4815); + expectedParticipant.setSessionId("theSessionId"); + expectedParticipantList.add(expectedParticipant); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2); + return null; + }).when(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + + List> users = new ArrayList<>(1); + Map user = new HashMap<>(); + user.put("inCall", 0); + user.put("lastPing", 4815); + user.put("roomId", 108); + user.put("sessionId", "theSessionId"); + user.put("userId", ""); + users.add(user); + signalingMessageReceiver.processUsersInRoom(users); + + InOrder inOrder = inOrder(mockedParticipantListMessageListener1, mockedParticipantListMessageListener2); + + inOrder.verify(mockedParticipantListMessageListener1).onUsersInRoom(expectedParticipantList); + inOrder.verify(mockedParticipantListMessageListener2).onUsersInRoom(expectedParticipantList); + } + + @Test + public void testExternalSignalingParticipantListMessageParticipantsUpdate() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + Map updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + List> users = new ArrayList<>(2); + Map user1 = new HashMap<>(); + user1.put("inCall", 7); + user1.put("lastPing", 4815); + 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. + user1.put("nextcloudSessionId", "theNextcloudSessionId"); + user1.put("participantPermissions", 42); + users.add(user1); + Map user2 = new HashMap<>(); + user2.put("inCall", 0); + user2.put("lastPing", 162342); + user2.put("sessionId", "theSessionId2"); + user2.put("participantType", 4); + users.add(user2); + updateMap.put("users", users); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + List expectedParticipantList = new ArrayList<>(2); + Participant expectedParticipant1 = new Participant(); + expectedParticipant1.setInCall(Participant.InCallFlags.IN_CALL | Participant.InCallFlags.WITH_AUDIO | Participant.InCallFlags.WITH_VIDEO); + expectedParticipant1.setLastPing(4815); + expectedParticipant1.setSessionId("theSessionId1"); + expectedParticipant1.setType(Participant.ParticipantType.USER); + expectedParticipant1.setUserId("theUserId"); + expectedParticipantList.add(expectedParticipant1); + + Participant expectedParticipant2 = new Participant(); + expectedParticipant2.setInCall(Participant.InCallFlags.DISCONNECTED); + expectedParticipant2.setLastPing(162342); + expectedParticipant2.setSessionId("theSessionId2"); + expectedParticipant2.setType(Participant.ParticipantType.GUEST); + expectedParticipantList.add(expectedParticipant2); + + verify(mockedParticipantListMessageListener, only()).onParticipantsUpdate(expectedParticipantList); + } + + @Test + public void testExternalSignalingParticipantListMessageAllParticipantsUpdate() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + Map updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("inCall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedParticipantListMessageListener, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + } + + @Test + public void testExternalSignalingParticipantListMessageAfterRemovingListener() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("inCall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verifyNoInteractions(mockedParticipantListMessageListener); + } + + @Test + public void testExternalSignalingParticipantListMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener3 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener3); + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("inCall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedParticipantListMessageListener1, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + verify(mockedParticipantListMessageListener3, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + verifyNoInteractions(mockedParticipantListMessageListener2); + } + + @Test + public void testExternalSignalingParticipantListMessageAfterAddingListenerAgain() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("inCall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedParticipantListMessageListener, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + } + + @Test + public void testAddParticipantListMessageListenerWhenHandlingExternalSignalingParticipantListMessage() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + return null; + }).when(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("inCall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + verify(mockedParticipantListMessageListener1, only()).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + verifyNoInteractions(mockedParticipantListMessageListener2); + } + + @Test + public void testRemoveParticipantListMessageListenerWhenHandlingExternalSignalingParticipantListMessage() { + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener1 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + SignalingMessageReceiver.ParticipantListMessageListener mockedParticipantListMessageListener2 = + mock(SignalingMessageReceiver.ParticipantListMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedParticipantListMessageListener2); + return null; + }).when(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + + signalingMessageReceiver.addListener(mockedParticipantListMessageListener1); + signalingMessageReceiver.addListener(mockedParticipantListMessageListener2); + + Map eventMap = new HashMap<>(); + eventMap.put("type", "update"); + eventMap.put("target", "participants"); + HashMap updateMap = new HashMap<>(); + updateMap.put("roomId", 108); + updateMap.put("all", true); + updateMap.put("inCall", 0); + eventMap.put("update", updateMap); + signalingMessageReceiver.processEvent(eventMap); + + InOrder inOrder = inOrder(mockedParticipantListMessageListener1, mockedParticipantListMessageListener2); + + inOrder.verify(mockedParticipantListMessageListener1).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + inOrder.verify(mockedParticipantListMessageListener2).onAllParticipantsUpdate(Participant.InCallFlags.DISCONNECTED); + } +}