From 45787caf0a8a35d08cb36ab9517d683e0ad874f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Mon, 17 Oct 2022 10:49:20 +0200 Subject: [PATCH] Add SignalingMessageReceiver class to listen to signaling messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now only WebRTC messages can be listened to, although it will be extended with other kinds later. This commit only introduces the base class, although it is not used yet anywhere; a concrete implementation will be added in a following commit. The test class is named "SignalingMessageReceiverWebRtcTest" rather than just "SignalingMessageReceiverTest" to have smaller, more manageable test classes for each listener kind rather than one large test class for all of them. Signed-off-by: Daniel Calviño Sánchez --- .../signaling/SignalingMessageReceiver.java | 222 +++++++++++ .../talk/signaling/WebRtcMessageNotifier.java | 120 ++++++ .../SignalingMessageReceiverWebRtcTest.java | 366 ++++++++++++++++++ 3 files changed, 708 insertions(+) create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java create mode 100644 app/src/main/java/com/nextcloud/talk/signaling/WebRtcMessageNotifier.java create mode 100644 app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverWebRtcTest.java diff --git a/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java new file mode 100644 index 000000000..f798ba9b8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java @@ -0,0 +1,222 @@ +/* + * 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.signaling.NCIceCandidate; +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; + +/** + * Hub to register listeners for signaling messages of different kinds. + * + * Adding and removing listeners, as well as notifying them is internally synchronized. This should be kept in mind + * if listeners are added or removed when handling an event to prevent deadlocks (nevertheless, just adding or + * removing a listener in the same thread handling the event is fine, and in most cases it will be fine too if done + * in a different thread, as long as the notifier thread is not forced to wait until the listener is added or removed). + * + * SignalingMessageReceiver does not fetch the signaling messages itself; subclasses must fetch them and then call + * the appropriate protected methods to process the messages and notify the listeners. + */ +public abstract class SignalingMessageReceiver { + + /** + * Listener for WebRTC messages. + * + * The messages are bound to a specific peer connection, so each listener is expected to handle messages only for + * a single peer connection. + */ + public interface WebRtcMessageListener { + void onOffer(String sdp, String nick); + void onAnswer(String sdp, String nick); + void onCandidate(String sdpMid, int sdpMLineIndex, String sdp); + void onEndOfCandidates(); + } + + private final WebRtcMessageNotifier webRtcMessageNotifier = new WebRtcMessageNotifier(); + + /** + * Adds a listener for WebRTC messages from the given session ID and room type. + * + * 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 or room type. + * + * @param listener the WebRtcMessageListener + * @param sessionId the ID of the session that messages come from + * @param roomType the room type that messages come from + */ + public void addListener(WebRtcMessageListener listener, String sessionId, String roomType) { + webRtcMessageNotifier.addListener(listener, sessionId, roomType); + } + + public void removeListener(WebRtcMessageListener listener) { + webRtcMessageNotifier.removeListener(listener); + } + + 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. + + String type = signalingMessage.getType(); + + String sessionId = signalingMessage.getFrom(); + String roomType = signalingMessage.getRoomType(); + + if ("offer".equals(type)) { + // Message schema (external signaling server): + // { + // "type": "message", + // "message": { + // "sender": { + // ... + // }, + // "data": { + // "to": #STRING#, + // "from": #STRING#, + // "type": "offer", + // "roomType": #STRING#, // "video" or "screen" + // "payload": { + // "type": "offer", + // "sdp": #STRING#, + // }, + // "sid": #STRING#, // external signaling server >= 0.5.0 + // }, + // }, + // } + // + // Message schema (internal signaling server): + // { + // "type": "message", + // "data": { + // "to": #STRING#, + // "sid": #STRING#, + // "roomType": #STRING#, // "video" or "screen" + // "type": "offer", + // "payload": { + // "type": "offer", + // "sdp": #STRING#, + // "nick": #STRING#, // Optional + // }, + // "from": #STRING#, + // }, + // } + + NCMessagePayload payload = signalingMessage.getPayload(); + if (payload == null) { + // Broken message, this should not happen. + return; + } + + String sdp = payload.getSdp(); + String nick = payload.getNick(); + + webRtcMessageNotifier.notifyOffer(sessionId, roomType, sdp, nick); + + return; + } + + if ("answer".equals(type)) { + // Message schema: same as offers, but with type "answer". + + NCMessagePayload payload = signalingMessage.getPayload(); + if (payload == null) { + // Broken message, this should not happen. + return; + } + + String sdp = payload.getSdp(); + String nick = payload.getNick(); + + webRtcMessageNotifier.notifyAnswer(sessionId, roomType, sdp, nick); + + return; + } + + if ("candidate".equals(type)) { + // Message schema (external signaling server): + // { + // "type": "message", + // "message": { + // "sender": { + // ... + // }, + // "data": { + // "to": #STRING#, + // "from": #STRING#, + // "type": "candidate", + // "roomType": #STRING#, // "video" or "screen" + // "payload": { + // "candidate": { + // "candidate": #STRING#, + // "sdpMid": #STRING#, + // "sdpMLineIndex": #INTEGER#, + // }, + // }, + // "sid": #STRING#, // external signaling server >= 0.5.0 + // }, + // }, + // } + // + // Message schema (internal signaling server): + // { + // "type": "message", + // "data": { + // "to": #STRING#, + // "sid": #STRING#, + // "roomType": #STRING#, // "video" or "screen" + // "type": "candidate", + // "payload": { + // "candidate": { + // "candidate": #STRING#, + // "sdpMid": #STRING#, + // "sdpMLineIndex": #INTEGER#, + // }, + // }, + // "from": #STRING#, + // }, + // } + + NCMessagePayload payload = signalingMessage.getPayload(); + if (payload == null) { + // Broken message, this should not happen. + return; + } + + NCIceCandidate ncIceCandidate = payload.getIceCandidate(); + if (ncIceCandidate == null) { + // Broken message, this should not happen. + return; + } + + webRtcMessageNotifier.notifyCandidate(sessionId, + roomType, + ncIceCandidate.getSdpMid(), + ncIceCandidate.getSdpMLineIndex(), + ncIceCandidate.getCandidate()); + + return; + } + + if ("endOfCandidates".equals(type)) { + webRtcMessageNotifier.notifyEndOfCandidates(sessionId, roomType); + + return; + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/signaling/WebRtcMessageNotifier.java b/app/src/main/java/com/nextcloud/talk/signaling/WebRtcMessageNotifier.java new file mode 100644 index 000000000..9dbe6cb38 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/signaling/WebRtcMessageNotifier.java @@ -0,0 +1,120 @@ +/* + * 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 java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Helper class to register and notify WebRtcMessageListeners. + * + * This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against + * a SignalingMessageReceiver rather than against a WebRtcMessageNotifier. + */ +class WebRtcMessageNotifier { + + /** + * Helper class to associate a WebRtcMessageListener with a session ID and room type. + */ + private static class WebRtcMessageListenerFrom { + public final SignalingMessageReceiver.WebRtcMessageListener listener; + public final String sessionId; + public final String roomType; + + private WebRtcMessageListenerFrom(SignalingMessageReceiver.WebRtcMessageListener listener, + String sessionId, + String roomType) { + this.listener = listener; + this.sessionId = sessionId; + this.roomType = roomType; + } + } + + private final List webRtcMessageListenersFrom = new ArrayList<>(); + + public synchronized void addListener(SignalingMessageReceiver.WebRtcMessageListener listener, String sessionId, String roomType) { + if (listener == null) { + throw new IllegalArgumentException("WebRtcMessageListener can not be null"); + } + + if (sessionId == null) { + throw new IllegalArgumentException("sessionId can not be null"); + } + + if (roomType == null) { + throw new IllegalArgumentException("roomType can not be null"); + } + + removeListener(listener); + + webRtcMessageListenersFrom.add(new WebRtcMessageListenerFrom(listener, sessionId, roomType)); + } + + public synchronized void removeListener(SignalingMessageReceiver.WebRtcMessageListener listener) { + Iterator it = webRtcMessageListenersFrom.iterator(); + while (it.hasNext()) { + WebRtcMessageListenerFrom listenerFrom = it.next(); + + if (listenerFrom.listener == listener) { + it.remove(); + + return; + } + } + } + + private List getListenersFor(String sessionId, String roomType) { + List webRtcMessageListeners = + new ArrayList<>(webRtcMessageListenersFrom.size()); + + for (WebRtcMessageListenerFrom listenerFrom : webRtcMessageListenersFrom) { + if (listenerFrom.sessionId.equals(sessionId) && listenerFrom.roomType.equals(roomType)) { + webRtcMessageListeners.add(listenerFrom.listener); + } + } + + return webRtcMessageListeners; + } + + public synchronized void notifyOffer(String sessionId, String roomType, String sdp, String nick) { + for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) { + listener.onOffer(sdp, nick); + } + } + + public synchronized void notifyAnswer(String sessionId, String roomType, String sdp, String nick) { + for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) { + listener.onAnswer(sdp, nick); + } + } + + public synchronized void notifyCandidate(String sessionId, String roomType, String sdpMid, int sdpMLineIndex, String sdp) { + for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) { + listener.onCandidate(sdpMid, sdpMLineIndex, sdp); + } + } + + public synchronized void notifyEndOfCandidates(String sessionId, String roomType) { + for (SignalingMessageReceiver.WebRtcMessageListener listener : getListenersFor(sessionId, roomType)) { + listener.onEndOfCandidates(); + } + } +} diff --git a/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverWebRtcTest.java b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverWebRtcTest.java new file mode 100644 index 000000000..da85f8493 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverWebRtcTest.java @@ -0,0 +1,366 @@ +/* + * 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.signaling.NCIceCandidate; +import com.nextcloud.talk.models.json.signaling.NCMessagePayload; +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 SignalingMessageReceiverWebRtcTest { + + 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 testAddWebRtcMessageListenerWithNullListener() { + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener(null, "theSessionId", "theRoomType"); + }); + } + + @Test + public void testAddWebRtcMessageListenerWithNullSessionId() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, null, "theRoomType"); + }); + } + + @Test + public void testAddWebRtcMessageListenerWithNullRoomType() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + Assert.assertThrows(IllegalArgumentException.class, () -> { + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", null); + }); + } + + @Test + public void testWebRtcMessageOffer() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onOffer("theSdp", null); + } + + @Test + public void testWebRtcMessageOfferWithNick() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("offer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("offer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onOffer("theSdp", "theNick"); + } + + @Test + public void testWebRtcMessageAnswer() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("answer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("answer"); + messagePayload.setSdp("theSdp"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onAnswer("theSdp", null); + } + + @Test + public void testWebRtcMessageAnswerWithNick() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("answer"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + messagePayload.setType("answer"); + messagePayload.setSdp("theSdp"); + messagePayload.setNick("theNick"); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onAnswer("theSdp", "theNick"); + } + + @Test + public void testWebRtcMessageCandidate() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("candidate"); + signalingMessage.setRoomType("theRoomType"); + NCMessagePayload messagePayload = new NCMessagePayload(); + NCIceCandidate iceCandidate = new NCIceCandidate(); + iceCandidate.setSdpMid("theSdpMid"); + iceCandidate.setSdpMLineIndex(42); + iceCandidate.setCandidate("theSdp"); + messagePayload.setIceCandidate(iceCandidate); + signalingMessage.setPayload(messagePayload); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onCandidate("theSdpMid", 42, "theSdp"); + } + + @Test + public void testWebRtcMessageEndOfCandidates() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onEndOfCandidates(); + } + + @Test + public void testWebRtcMessageSeveralListenersSameFrom() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates(); + verify(mockedWebRtcMessageListener2, only()).onEndOfCandidates(); + } + + @Test + public void testWebRtcMessageNotMatchingSessionId() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("notMatchingSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedWebRtcMessageListener); + } + + @Test + public void testWebRtcMessageNotMatchingRoomType() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("notMatchingRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedWebRtcMessageListener); + } + + @Test + public void testWebRtcMessageAfterRemovingListener() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + signalingMessageReceiver.removeListener(mockedWebRtcMessageListener); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedWebRtcMessageListener); + } + + @Test + public void testWebRtcMessageAfterRemovingSingleListenerOfSeveral() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener3 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener3, "theSessionId", "theRoomType"); + signalingMessageReceiver.removeListener(mockedWebRtcMessageListener2); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates(); + verify(mockedWebRtcMessageListener3, only()).onEndOfCandidates(); + verifyNoInteractions(mockedWebRtcMessageListener2); + } + + @Test + public void testWebRtcMessageAfterAddingListenerAgainForDifferentFrom() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener, "theSessionId2", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verifyNoInteractions(mockedWebRtcMessageListener); + + signalingMessage.setFrom("theSessionId2"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener, only()).onEndOfCandidates(); + } + + @Test + public void testAddWebRtcMessageListenerWhenHandlingWebRtcMessage() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType"); + return null; + }).when(mockedWebRtcMessageListener1).onEndOfCandidates(); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + verify(mockedWebRtcMessageListener1, only()).onEndOfCandidates(); + verifyNoInteractions(mockedWebRtcMessageListener2); + } + + @Test + public void testRemoveWebRtcMessageListenerWhenHandlingWebRtcMessage() { + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener1 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + SignalingMessageReceiver.WebRtcMessageListener mockedWebRtcMessageListener2 = + mock(SignalingMessageReceiver.WebRtcMessageListener.class); + + doAnswer((invocation) -> { + signalingMessageReceiver.removeListener(mockedWebRtcMessageListener2); + return null; + }).when(mockedWebRtcMessageListener1).onEndOfCandidates(); + + signalingMessageReceiver.addListener(mockedWebRtcMessageListener1, "theSessionId", "theRoomType"); + signalingMessageReceiver.addListener(mockedWebRtcMessageListener2, "theSessionId", "theRoomType"); + + NCSignalingMessage signalingMessage = new NCSignalingMessage(); + signalingMessage.setFrom("theSessionId"); + signalingMessage.setType("endOfCandidates"); + signalingMessage.setRoomType("theRoomType"); + signalingMessageReceiver.processSignalingMessage(signalingMessage); + + InOrder inOrder = inOrder(mockedWebRtcMessageListener1, mockedWebRtcMessageListener2); + + inOrder.verify(mockedWebRtcMessageListener1).onEndOfCandidates(); + inOrder.verify(mockedWebRtcMessageListener2).onEndOfCandidates(); + } +}