From ea2bebe3b0f91b61147822e51cfecbf7baa0bc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Thu, 19 Dec 2024 12:17:41 +0100 Subject: [PATCH] Add support for sending signaling messages in the MessageSender MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Daniel Calviño Sánchez --- .../nextcloud/talk/activities/CallActivity.kt | 10 ++ .../nextcloud/talk/call/MessageSender.java | 47 +++++- .../nextcloud/talk/call/MessageSenderMcu.java | 8 +- .../talk/call/MessageSenderNoMcu.java | 8 +- .../talk/call/MessageSenderMcuTest.kt | 12 +- .../talk/call/MessageSenderNoMcuTest.kt | 7 +- .../nextcloud/talk/call/MessageSenderTest.kt | 134 ++++++++++++++++++ 7 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index e8472e973..7a4fecb25 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -1590,6 +1590,8 @@ class CallActivity : CallBaseActivity() { hasMCU = false messageSender = MessageSenderNoMcu( + signalingMessageSender, + callParticipants.keys, peerConnectionWrapperList ) @@ -1895,11 +1897,15 @@ class CallActivity : CallBaseActivity() { if (hasMCU) { messageSender = MessageSenderMcu( + signalingMessageSender, + callParticipants.keys, peerConnectionWrapperList, webSocketClient!!.sessionId ) } else { messageSender = MessageSenderNoMcu( + signalingMessageSender, + callParticipants.keys, peerConnectionWrapperList ) } @@ -1934,11 +1940,15 @@ class CallActivity : CallBaseActivity() { if (hasMCU) { messageSender = MessageSenderMcu( + signalingMessageSender, + callParticipants.keys, peerConnectionWrapperList, webSocketClient!!.sessionId ) } else { messageSender = MessageSenderNoMcu( + signalingMessageSender, + callParticipants.keys, peerConnectionWrapperList ) } diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSender.java b/app/src/main/java/com/nextcloud/talk/call/MessageSender.java index a868fc6b8..dd3eb149a 100644 --- a/app/src/main/java/com/nextcloud/talk/call/MessageSender.java +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSender.java @@ -7,16 +7,22 @@ package com.nextcloud.talk.call; import com.nextcloud.talk.models.json.signaling.DataChannelMessage; +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; +import com.nextcloud.talk.signaling.SignalingMessageSender; import com.nextcloud.talk.webrtc.PeerConnectionWrapper; import java.util.List; +import java.util.Set; /** * Helper class to send messages to participants in a call. *

* A specific subclass has to be created depending on whether an MCU is being used or not. *

- * Note that, unlike signaling messages, data channel messages require a peer connection. Therefore data channel + * Note that recipients of signaling messages are not validated, so no error will be triggered if trying to send a + * message to a participant with a session ID that does not exist or is not in the call. + *

+ * Also note that, unlike signaling messages, data channel messages require a peer connection. Therefore data channel * messages may not be received by a participant if there is no peer connection with that participant (for example, if * neither the local and remote participants have publishing rights). Moreover, data channel messages are expected to * be received only on peer connections with type "video", so data channel messages will not be sent on other peer @@ -24,9 +30,17 @@ import java.util.List; */ public abstract class MessageSender { + private final SignalingMessageSender signalingMessageSender; + + private final Set callParticipantSessionIds; + protected final List peerConnectionWrappers; - public MessageSender(List peerConnectionWrappers) { + public MessageSender(SignalingMessageSender signalingMessageSender, + Set callParticipantSessionIds, + List peerConnectionWrappers) { + this.signalingMessageSender = signalingMessageSender; + this.callParticipantSessionIds = callParticipantSessionIds; this.peerConnectionWrappers = peerConnectionWrappers; } @@ -37,6 +51,35 @@ public abstract class MessageSender { */ public abstract void sendToAll(DataChannelMessage dataChannelMessage); + /** + * Sends the given signaling message to the given session ID. + *

+ * Note that the signaling message will be modified to set the recipient in the "to" field. + * + * @param ncSignalingMessage the message to send + * @param sessionId the signaling session ID of the participant to send the message to + */ + public void send(NCSignalingMessage ncSignalingMessage, String sessionId) { + ncSignalingMessage.setTo(sessionId); + + signalingMessageSender.send(ncSignalingMessage); + } + + /** + * Sends the given signaling message to all the participants in the call. + *

+ * Note that the signaling message will be modified to set each of the recipients in the "to" field. + * + * @param ncSignalingMessage the message to send + */ + public void sendToAll(NCSignalingMessage ncSignalingMessage) { + for (String sessionId: callParticipantSessionIds) { + ncSignalingMessage.setTo(sessionId); + + signalingMessageSender.send(ncSignalingMessage); + } + } + protected PeerConnectionWrapper getPeerConnectionWrapper(String sessionId) { for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) { if (peerConnectionWrapper.getSessionId().equals(sessionId) diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java b/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java index f3278fe4c..0b7d3eaee 100644 --- a/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java @@ -7,9 +7,11 @@ package com.nextcloud.talk.call; import com.nextcloud.talk.models.json.signaling.DataChannelMessage; +import com.nextcloud.talk.signaling.SignalingMessageSender; import com.nextcloud.talk.webrtc.PeerConnectionWrapper; import java.util.List; +import java.util.Set; /** * Helper class to send messages to participants in a call when an MCU is used. @@ -21,9 +23,11 @@ public class MessageSenderMcu extends MessageSender { private final String ownSessionId; - public MessageSenderMcu(List peerConnectionWrappers, + public MessageSenderMcu(SignalingMessageSender signalingMessageSender, + Set callParticipantSessionIds, + List peerConnectionWrappers, String ownSessionId) { - super(peerConnectionWrappers); + super(signalingMessageSender, callParticipantSessionIds, peerConnectionWrappers); this.ownSessionId = ownSessionId; } diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java b/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java index b5b903503..d6c837bb7 100644 --- a/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java @@ -7,17 +7,21 @@ package com.nextcloud.talk.call; import com.nextcloud.talk.models.json.signaling.DataChannelMessage; +import com.nextcloud.talk.signaling.SignalingMessageSender; import com.nextcloud.talk.webrtc.PeerConnectionWrapper; import java.util.List; +import java.util.Set; /** * Helper class to send messages to participants in a call when an MCU is not used. */ public class MessageSenderNoMcu extends MessageSender { - public MessageSenderNoMcu(List peerConnectionWrappers) { - super(peerConnectionWrappers); + public MessageSenderNoMcu(SignalingMessageSender signalingMessageSender, + Set callParticipantSessionIds, + List peerConnectionWrappers) { + super(signalingMessageSender, callParticipantSessionIds, peerConnectionWrappers); } /** diff --git a/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt b/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt index 49ca9e864..9fd8d6289 100644 --- a/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt +++ b/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt @@ -7,6 +7,7 @@ package com.nextcloud.talk.call import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.signaling.SignalingMessageSender import com.nextcloud.talk.webrtc.PeerConnectionWrapper import org.junit.Before import org.junit.Test @@ -27,6 +28,10 @@ class MessageSenderMcuTest { @Before fun setUp() { + val signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java) + + val callParticipants = HashMap() + peerConnectionWrappers = ArrayList() peerConnectionWrapper1 = Mockito.mock(PeerConnectionWrapper::class.java) @@ -59,7 +64,12 @@ class MessageSenderMcuTest { Mockito.`when`(ownPeerConnectionWrapperScreen!!.videoStreamType).thenReturn("screen") peerConnectionWrappers!!.add(ownPeerConnectionWrapperScreen) - messageSender = MessageSenderMcu(peerConnectionWrappers, "ownSessionId") + messageSender = MessageSenderMcu( + signalingMessageSender, + callParticipants.keys, + peerConnectionWrappers, + "ownSessionId" + ) } @Test diff --git a/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt b/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt index 6784bcef3..303108ed9 100644 --- a/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt +++ b/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt @@ -7,6 +7,7 @@ package com.nextcloud.talk.call import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.signaling.SignalingMessageSender import com.nextcloud.talk.webrtc.PeerConnectionWrapper import org.junit.Before import org.junit.Test @@ -25,6 +26,10 @@ class MessageSenderNoMcuTest { @Before fun setUp() { + val signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java) + + val callParticipants = HashMap() + peerConnectionWrappers = ArrayList() peerConnectionWrapper1 = Mockito.mock(PeerConnectionWrapper::class.java) @@ -47,7 +52,7 @@ class MessageSenderNoMcuTest { Mockito.`when`(peerConnectionWrapper4Screen!!.videoStreamType).thenReturn("screen") peerConnectionWrappers!!.add(peerConnectionWrapper4Screen) - messageSender = MessageSenderNoMcu(peerConnectionWrappers) + messageSender = MessageSenderNoMcu(signalingMessageSender, callParticipants.keys, peerConnectionWrappers) } @Test diff --git a/app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt b/app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt new file mode 100644 index 000000000..46915ef40 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/MessageSenderTest.kt @@ -0,0 +1,134 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package com.nextcloud.talk.call + +import com.nextcloud.talk.models.json.signaling.DataChannelMessage +import com.nextcloud.talk.models.json.signaling.NCSignalingMessage +import com.nextcloud.talk.signaling.SignalingMessageSender +import com.nextcloud.talk.webrtc.PeerConnectionWrapper +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.any +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.times +import org.mockito.invocation.InvocationOnMock + +class MessageSenderTest { + + private class MessageSender( + signalingMessageSender: SignalingMessageSender?, + callParticipantSessionIds: Set?, + peerConnectionWrappers: List? + ) : com.nextcloud.talk.call.MessageSender( + signalingMessageSender, + callParticipantSessionIds, + peerConnectionWrappers + ) { + + override fun sendToAll(dataChannelMessage: DataChannelMessage?) { + // Not used in base class tests + } + } + + private var signalingMessageSender: SignalingMessageSender? = null + + private var callParticipants: MutableMap? = null + + private var messageSender: MessageSender? = null + + @Before + fun setUp() { + signalingMessageSender = Mockito.mock(SignalingMessageSender::class.java) + + callParticipants = HashMap() + + val callParticipant1: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId1"] = callParticipant1 + + val callParticipant2: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId2"] = callParticipant2 + + val callParticipant3: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId3"] = callParticipant3 + + val callParticipant4: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId4"] = callParticipant4 + + val peerConnectionWrappers = ArrayList() + + messageSender = MessageSender(signalingMessageSender, callParticipants!!.keys, peerConnectionWrappers) + } + + @Test + fun testSendSignalingMessage() { + val message: NCSignalingMessage = Mockito.mock(NCSignalingMessage::class.java) + messageSender!!.send(message, "theSessionId2") + + Mockito.verify(message).to = "theSessionId2" + Mockito.verify(signalingMessageSender!!).send(message) + } + + @Test + fun testSendSignalingMessageIfUnknownSessionId() { + val message: NCSignalingMessage = Mockito.mock(NCSignalingMessage::class.java) + messageSender!!.send(message, "unknownSessionId") + + Mockito.verify(message).to = "unknownSessionId" + Mockito.verify(signalingMessageSender!!).send(message) + } + + @Test + fun testSendSignalingMessageToAll() { + val sentTo: MutableList = ArrayList() + doAnswer { invocation: InvocationOnMock -> + val arguments = invocation.arguments + val message = (arguments[0] as NCSignalingMessage) + + sentTo.add(message.to) + null + }.`when`(signalingMessageSender!!).send(any()) + + val message = NCSignalingMessage() + messageSender!!.sendToAll(message) + + assertTrue(sentTo.contains("theSessionId1")) + assertTrue(sentTo.contains("theSessionId2")) + assertTrue(sentTo.contains("theSessionId3")) + assertTrue(sentTo.contains("theSessionId4")) + Mockito.verify(signalingMessageSender!!, times(4)).send(message) + Mockito.verifyNoMoreInteractions(signalingMessageSender) + } + + @Test + fun testSendSignalingMessageToAllWhenParticipantsWereUpdated() { + val callParticipant5: CallParticipant = Mockito.mock(CallParticipant::class.java) + callParticipants!!["theSessionId5"] = callParticipant5 + + callParticipants!!.remove("theSessionId2") + callParticipants!!.remove("theSessionId3") + + val sentTo: MutableList = ArrayList() + doAnswer { invocation: InvocationOnMock -> + val arguments = invocation.arguments + val message = (arguments[0] as NCSignalingMessage) + + sentTo.add(message.to) + null + }.`when`(signalingMessageSender!!).send(any()) + + val message = NCSignalingMessage() + messageSender!!.sendToAll(message) + + assertTrue(sentTo.contains("theSessionId1")) + assertTrue(sentTo.contains("theSessionId4")) + assertTrue(sentTo.contains("theSessionId5")) + Mockito.verify(signalingMessageSender!!, times(3)).send(message) + Mockito.verifyNoMoreInteractions(signalingMessageSender) + } +}