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) + } +}