From 3e36c85015ec57cbc536e3388d7c838bdd78cc61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Fri, 25 Oct 2024 14:37:37 +0200 Subject: [PATCH] Add helper class to send messages to call participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now it just provides support for sending a data channel message to all participants, so notifying all participants when the media is toggled or the speaking status change can be directly refactored to use it. While it would have been fine to use a single class for both MCU and no MCU they were split for easier and cleaner unit testing in future stages. Signed-off-by: Daniel Calviño Sánchez --- .../nextcloud/talk/activities/CallActivity.kt | 56 ++++++++------- .../nextcloud/talk/call/MessageSender.java | 47 +++++++++++++ .../nextcloud/talk/call/MessageSenderMcu.java | 34 ++++++++++ .../talk/call/MessageSenderNoMcu.java | 28 ++++++++ .../talk/call/MessageSenderMcuTest.kt | 68 +++++++++++++++++++ .../talk/call/MessageSenderNoMcuTest.kt | 48 +++++++++++++ 6 files changed, 257 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/call/MessageSender.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java create mode 100644 app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt create mode 100644 app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.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 c6a2e9838..a586de632 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -63,6 +63,9 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedA import com.nextcloud.talk.call.CallParticipant import com.nextcloud.talk.call.CallParticipantList import com.nextcloud.talk.call.CallParticipantModel +import com.nextcloud.talk.call.MessageSender +import com.nextcloud.talk.call.MessageSenderMcu +import com.nextcloud.talk.call.MessageSenderNoMcu import com.nextcloud.talk.call.ReactionAnimator import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.data.user.model.User @@ -242,6 +245,7 @@ class CallActivity : CallBaseActivity() { private var signalingMessageReceiver: SignalingMessageReceiver? = null private val internalSignalingMessageSender = InternalSignalingMessageSender() private var signalingMessageSender: SignalingMessageSender? = null + private var messageSender: MessageSender? = null private val offerAnswerNickProviders: MutableMap = HashMap() private val callParticipantMessageListeners: MutableMap = HashMap() private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver() @@ -1172,18 +1176,7 @@ class CallActivity : CallBaseActivity() { if (isSpeaking) SIGNALING_MESSAGE_SPEAKING_STARTED else SIGNALING_MESSAGE_SPEAKING_STOPPED if (isConnectionEstablished && othersInCall) { - if (!hasMCU) { - for (peerConnectionWrapper in peerConnectionWrapperList) { - peerConnectionWrapper.send(DataChannelMessage(isSpeakingMessage)) - } - } else { - for (peerConnectionWrapper in peerConnectionWrapperList) { - if (peerConnectionWrapper.sessionId == webSocketClient!!.sessionId) { - peerConnectionWrapper.send(DataChannelMessage(isSpeakingMessage)) - break - } - } - } + messageSender!!.sendToAll(DataChannelMessage(isSpeakingMessage)) } } @@ -1368,18 +1361,7 @@ class CallActivity : CallBaseActivity() { } } if (isConnectionEstablished) { - if (!hasMCU) { - for (peerConnectionWrapper in peerConnectionWrapperList) { - peerConnectionWrapper.send(DataChannelMessage(message)) - } - } else { - for (peerConnectionWrapper in peerConnectionWrapperList) { - if (peerConnectionWrapper.sessionId == webSocketClient!!.sessionId) { - peerConnectionWrapper.send(DataChannelMessage(message)) - break - } - } - } + messageSender!!.sendToAll(DataChannelMessage(message)) } } @@ -1621,6 +1603,10 @@ class CallActivity : CallBaseActivity() { hasMCU = false + messageSender = MessageSenderNoMcu( + peerConnectionWrapperList + ) + joinRoomAndCall() } } @@ -1911,6 +1897,17 @@ class CallActivity : CallBaseActivity() { // be overwritten with the right value once the response to the "hello" message is received. hasMCU = webSocketClient!!.hasMCU() Log.d(TAG, "hasMCU is $hasMCU") + + if (hasMCU) { + messageSender = MessageSenderMcu( + peerConnectionWrapperList, + webSocketClient!!.sessionId + ) + } else { + messageSender = MessageSenderNoMcu( + peerConnectionWrapperList + ) + } } else { if (webSocketClient!!.isConnected && currentCallStatus === CallStatus.PUBLISHER_FAILED) { webSocketClient!!.restartWebSocket() @@ -1940,6 +1937,17 @@ class CallActivity : CallBaseActivity() { hasMCU = webSocketClient!!.hasMCU() Log.d(TAG, "hasMCU is $hasMCU") + if (hasMCU) { + messageSender = MessageSenderMcu( + peerConnectionWrapperList, + webSocketClient!!.sessionId + ) + } else { + messageSender = MessageSenderNoMcu( + peerConnectionWrapperList + ) + } + if (!webSocketCommunicationEvent.getHashMap()!!.containsKey("oldResumeId")) { if (currentCallStatus === CallStatus.RECONNECTING) { hangup(false, false) diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSender.java b/app/src/main/java/com/nextcloud/talk/call/MessageSender.java new file mode 100644 index 000000000..2e0820965 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSender.java @@ -0,0 +1,47 @@ +/* + * 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.webrtc.PeerConnectionWrapper; + +import java.util.List; + +/** + * 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 + * 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). + */ +public abstract class MessageSender { + + protected final List peerConnectionWrappers; + + public MessageSender(List peerConnectionWrappers) { + this.peerConnectionWrappers = peerConnectionWrappers; + } + + /** + * Sends the given data channel message to all the participants in the call. + * + * @param dataChannelMessage the message to send + */ + public abstract void sendToAll(DataChannelMessage dataChannelMessage); + + protected PeerConnectionWrapper getPeerConnectionWrapper(String sessionId) { + for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) { + if (peerConnectionWrapper.getSessionId().equals(sessionId)) { + return peerConnectionWrapper; + } + } + + return null; + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java b/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java new file mode 100644 index 000000000..803becf57 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSenderMcu.java @@ -0,0 +1,34 @@ +/* + * 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.webrtc.PeerConnectionWrapper; + +import java.util.List; + +/** + * Helper class to send messages to participants in a call when an MCU is used. + */ +public class MessageSenderMcu extends MessageSender { + + private final String ownSessionId; + + public MessageSenderMcu(List peerConnectionWrappers, + String ownSessionId) { + super(peerConnectionWrappers); + + this.ownSessionId = ownSessionId; + } + + public void sendToAll(DataChannelMessage dataChannelMessage) { + PeerConnectionWrapper ownPeerConnectionWrapper = getPeerConnectionWrapper(ownSessionId); + if (ownPeerConnectionWrapper != null) { + ownPeerConnectionWrapper.send(dataChannelMessage); + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java b/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java new file mode 100644 index 000000000..7dea72926 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/MessageSenderNoMcu.java @@ -0,0 +1,28 @@ +/* + * 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.webrtc.PeerConnectionWrapper; + +import java.util.List; + +/** + * 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 void sendToAll(DataChannelMessage dataChannelMessage) { + for (PeerConnectionWrapper peerConnectionWrapper: peerConnectionWrappers) { + peerConnectionWrapper.send(dataChannelMessage); + } + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt b/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt new file mode 100644 index 000000000..9bdefbfe8 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/MessageSenderMcuTest.kt @@ -0,0 +1,68 @@ +/* + * 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.webrtc.PeerConnectionWrapper +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.never + +class MessageSenderMcuTest { + + private var peerConnectionWrappers: MutableList? = null + private var peerConnectionWrapper1: PeerConnectionWrapper? = null + private var peerConnectionWrapper2: PeerConnectionWrapper? = null + private var ownPeerConnectionWrapper: PeerConnectionWrapper? = null + + private var messageSender: MessageSenderMcu? = null + + @Before + fun setUp() { + peerConnectionWrappers = ArrayList() + + peerConnectionWrapper1 = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper1!!.sessionId).thenReturn("theSessionId1") + Mockito.`when`(peerConnectionWrapper1!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(peerConnectionWrapper1) + + peerConnectionWrapper2 = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper2!!.sessionId).thenReturn("theSessionId2") + Mockito.`when`(peerConnectionWrapper2!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(peerConnectionWrapper2) + + ownPeerConnectionWrapper = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(ownPeerConnectionWrapper!!.sessionId).thenReturn("ownSessionId") + Mockito.`when`(ownPeerConnectionWrapper!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(ownPeerConnectionWrapper) + + messageSender = MessageSenderMcu(peerConnectionWrappers, "ownSessionId") + } + + @Test + fun testSendDataChannelMessageToAll() { + val message = DataChannelMessage() + messageSender!!.sendToAll(message) + + Mockito.verify(ownPeerConnectionWrapper!!).send(message) + Mockito.verify(peerConnectionWrapper1!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2!!, never()).send(message) + } + + @Test + fun testSendDataChannelMessageToAllWithoutOwnPeerConnection() { + peerConnectionWrappers!!.remove(ownPeerConnectionWrapper) + + val message = DataChannelMessage() + messageSender!!.sendToAll(message) + + Mockito.verify(ownPeerConnectionWrapper!!, never()).send(message) + Mockito.verify(peerConnectionWrapper1!!, never()).send(message) + Mockito.verify(peerConnectionWrapper2!!, never()).send(message) + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt b/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt new file mode 100644 index 000000000..cec1534de --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/MessageSenderNoMcuTest.kt @@ -0,0 +1,48 @@ +/* + * 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.webrtc.PeerConnectionWrapper +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito + +class MessageSenderNoMcuTest { + + private var peerConnectionWrappers: MutableList? = null + private var peerConnectionWrapper1: PeerConnectionWrapper? = null + private var peerConnectionWrapper2: PeerConnectionWrapper? = null + + private var messageSender: MessageSenderNoMcu? = null + + @Before + fun setUp() { + peerConnectionWrappers = ArrayList() + + peerConnectionWrapper1 = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper1!!.sessionId).thenReturn("theSessionId1") + Mockito.`when`(peerConnectionWrapper1!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(peerConnectionWrapper1) + + peerConnectionWrapper2 = Mockito.mock(PeerConnectionWrapper::class.java) + Mockito.`when`(peerConnectionWrapper2!!.sessionId).thenReturn("theSessionId2") + Mockito.`when`(peerConnectionWrapper2!!.videoStreamType).thenReturn("video") + peerConnectionWrappers!!.add(peerConnectionWrapper2) + + messageSender = MessageSenderNoMcu(peerConnectionWrappers) + } + + @Test + fun testSendDataChannelMessageToAll() { + val message = DataChannelMessage() + messageSender!!.sendToAll(message) + + Mockito.verify(peerConnectionWrapper1!!).send(message) + Mockito.verify(peerConnectionWrapper2!!).send(message) + } +}