Add listener for local participant signaling messages

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This commit is contained in:
Daniel Calviño Sánchez 2023-01-26 13:33:07 +01:00 committed by Marcel Hibbe
parent 6a799387c8
commit 747a4646d3
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
4 changed files with 327 additions and 3 deletions

View File

@ -0,0 +1,53 @@
/*
* Nextcloud Talk application
*
* @author Daniel Calviño Sánchez
* Copyright (C) 2023 Daniel Calviño Sánchez <danxuliu@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.signaling;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Helper class to register and notify LocalParticipantMessageListeners.
*
* This class is only meant for internal use by SignalingMessageReceiver; listeners must register themselves against
* a SignalingMessageReceiver rather than against a LocalParticipantMessageNotifier.
*/
class LocalParticipantMessageNotifier {
private final Set<SignalingMessageReceiver.LocalParticipantMessageListener> localParticipantMessageListeners = new LinkedHashSet<>();
public synchronized void addListener(SignalingMessageReceiver.LocalParticipantMessageListener listener) {
if (listener == null) {
throw new IllegalArgumentException("localParticipantMessageListener can not be null");
}
localParticipantMessageListeners.add(listener);
}
public synchronized void removeListener(SignalingMessageReceiver.LocalParticipantMessageListener listener) {
localParticipantMessageListeners.remove(listener);
}
public synchronized void notifySwitchTo(String token) {
for (SignalingMessageReceiver.LocalParticipantMessageListener listener : new ArrayList<>(localParticipantMessageListeners)) {
listener.onSwitchTo(token);
}
}
}

View File

@ -118,6 +118,26 @@ public abstract class SignalingMessageReceiver {
void onAllParticipantsUpdate(long inCall);
}
/**
* Listener for local participant messages.
*
* The messages are implicitly bound to the local participant (or, rather, its session); listeners are expected
* to know the local participant.
*
* The messages are related to the conversation, so the local participant may or may not be in a call when they
* are received.
*/
public interface LocalParticipantMessageListener {
/**
* Request for the client to switch to the given conversation.
*
* This message is received only when the external signaling server is used.
*
* @param token the token of the conversation to switch to.
*/
void onSwitchTo(String token);
}
/**
* Listener for call participant messages.
*
@ -160,6 +180,8 @@ public abstract class SignalingMessageReceiver {
private final ParticipantListMessageNotifier participantListMessageNotifier = new ParticipantListMessageNotifier();
private final LocalParticipantMessageNotifier localParticipantMessageNotifier = new LocalParticipantMessageNotifier();
private final CallParticipantMessageNotifier callParticipantMessageNotifier = new CallParticipantMessageNotifier();
private final OfferMessageNotifier offerMessageNotifier = new OfferMessageNotifier();
@ -181,6 +203,21 @@ public abstract class SignalingMessageReceiver {
participantListMessageNotifier.removeListener(listener);
}
/**
* Adds a listener for local participant 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 LocalParticipantMessageListener
*/
public void addListener(LocalParticipantMessageListener listener) {
localParticipantMessageNotifier.addListener(listener);
}
public void removeListener(LocalParticipantMessageListener listener) {
localParticipantMessageNotifier.removeListener(listener);
}
/**
* Adds a listener for call participant messages.
*
@ -232,17 +269,56 @@ public abstract class SignalingMessageReceiver {
}
protected void processEvent(Map<String, Object> eventMap) {
if (!"participants".equals(eventMap.get("target"))) {
if ("room".equals(eventMap.get("target")) && "switchto".equals(eventMap.get("type"))) {
processSwitchToEvent(eventMap);
return;
}
if ("update".equals(eventMap.get("type"))) {
if ("participants".equals(eventMap.get("target")) && "update".equals(eventMap.get("type"))) {
processUpdateEvent(eventMap);
return;
}
}
private void processSwitchToEvent(Map<String, Object> eventMap) {
// Message schema:
// {
// "type": "event",
// "event": {
// "target": "room",
// "type": "switchto",
// "switchto": {
// "roomid": #STRING#,
// },
// },
// }
Map<String, Object> switchToMap;
try {
switchToMap = (Map<String, Object>) eventMap.get("switchto");
} catch (RuntimeException e) {
// Broken message, this should not happen.
return;
}
if (switchToMap == null) {
// Broken message, this should not happen.
return;
}
String token;
try {
token = switchToMap.get("roomid").toString();
} catch (RuntimeException e) {
// Broken message, this should not happen.
return;
}
localParticipantMessageNotifier.notifySwitchTo(token);
}
private void processUpdateEvent(Map<String, Object> eventMap) {
Map<String, Object> updateMap;
try {

View File

@ -201,12 +201,14 @@ class WebSocketInstance internal constructor(
val target = eventOverallWebSocketMessage.eventMap!!["target"] as String?
if (target != null) {
when (target) {
Globals.TARGET_ROOM ->
Globals.TARGET_ROOM -> {
if ("message" == eventOverallWebSocketMessage.eventMap!!["type"]) {
processRoomMessageMessage(eventOverallWebSocketMessage)
} else if ("join" == eventOverallWebSocketMessage.eventMap!!["type"]) {
processRoomJoinMessage(eventOverallWebSocketMessage)
}
signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap)
}
Globals.TARGET_PARTICIPANTS ->
signalingMessageReceiver.process(eventOverallWebSocketMessage.eventMap)
else ->

View File

@ -0,0 +1,193 @@
/*
* Nextcloud Talk application
*
* @author Daniel Calviño Sánchez
* Copyright (C) 2023 Daniel Calviño Sánchez <danxuliu@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.signaling;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import java.util.HashMap;
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 SignalingMessageReceiverLocalParticipantTest {
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 testAddLocalParticipantMessageListenerWithNullListener() {
Assert.assertThrows(IllegalArgumentException.class, () -> {
signalingMessageReceiver.addListener((SignalingMessageReceiver.LocalParticipantMessageListener) null);
});
}
@Test
public void testExternalSignalingLocalParticipantMessageSwitchTo() {
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "switchto");
eventMap.put("target", "room");
Map<String, Object> switchToMap = new HashMap<>();
switchToMap.put("roomid", "theToken");
eventMap.put("switchto", switchToMap);
signalingMessageReceiver.processEvent(eventMap);
verify(mockedLocalParticipantMessageListener, only()).onSwitchTo("theToken");
}
@Test
public void testExternalSignalingLocalParticipantMessageAfterRemovingListener() {
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener);
signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "switchto");
eventMap.put("target", "room");
HashMap<String, Object> switchToMap = new HashMap<>();
switchToMap.put("roomid", "theToken");
eventMap.put("switchto", switchToMap);
signalingMessageReceiver.processEvent(eventMap);
verifyNoInteractions(mockedLocalParticipantMessageListener);
}
@Test
public void testExternalSignalingLocalParticipantMessageAfterRemovingSingleListenerOfSeveral() {
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener3 =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1);
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2);
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener3);
signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener2);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "switchto");
eventMap.put("target", "room");
HashMap<String, Object> switchToMap = new HashMap<>();
switchToMap.put("roomid", "theToken");
eventMap.put("switchto", switchToMap);
signalingMessageReceiver.processEvent(eventMap);
verify(mockedLocalParticipantMessageListener1, only()).onSwitchTo("theToken");
verify(mockedLocalParticipantMessageListener3, only()).onSwitchTo("theToken");
verifyNoInteractions(mockedLocalParticipantMessageListener2);
}
@Test
public void testExternalSignalingLocalParticipantMessageAfterAddingListenerAgain() {
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener);
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "switchto");
eventMap.put("target", "room");
HashMap<String, Object> switchToMap = new HashMap<>();
switchToMap.put("roomid", "theToken");
eventMap.put("switchto", switchToMap);
signalingMessageReceiver.processEvent(eventMap);
verify(mockedLocalParticipantMessageListener, only()).onSwitchTo("theToken");
}
@Test
public void testAddLocalParticipantMessageListenerWhenHandlingExternalSignalingLocalParticipantMessage() {
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
doAnswer((invocation) -> {
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2);
return null;
}).when(mockedLocalParticipantMessageListener1).onSwitchTo("theToken");
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "switchto");
eventMap.put("target", "room");
HashMap<String, Object> switchToMap = new HashMap<>();
switchToMap.put("roomid", "theToken");
eventMap.put("switchto", switchToMap);
signalingMessageReceiver.processEvent(eventMap);
verify(mockedLocalParticipantMessageListener1, only()).onSwitchTo("theToken");
verifyNoInteractions(mockedLocalParticipantMessageListener2);
}
@Test
public void testRemoveLocalParticipantMessageListenerWhenHandlingExternalSignalingLocalParticipantMessage() {
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener1 =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
SignalingMessageReceiver.LocalParticipantMessageListener mockedLocalParticipantMessageListener2 =
mock(SignalingMessageReceiver.LocalParticipantMessageListener.class);
doAnswer((invocation) -> {
signalingMessageReceiver.removeListener(mockedLocalParticipantMessageListener2);
return null;
}).when(mockedLocalParticipantMessageListener1).onSwitchTo("theToken");
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener1);
signalingMessageReceiver.addListener(mockedLocalParticipantMessageListener2);
Map<String, Object> eventMap = new HashMap<>();
eventMap.put("type", "switchto");
eventMap.put("target", "room");
HashMap<String, Object> switchToMap = new HashMap<>();
switchToMap.put("roomid", "theToken");
eventMap.put("switchto", switchToMap);
signalingMessageReceiver.processEvent(eventMap);
InOrder inOrder = inOrder(mockedLocalParticipantMessageListener1, mockedLocalParticipantMessageListener2);
inOrder.verify(mockedLocalParticipantMessageListener1).onSwitchTo("theToken");
inOrder.verify(mockedLocalParticipantMessageListener2).onSwitchTo("theToken");
}
}