From ab72db7a10538f12c0213904f1527f8c5058a71c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= Date: Tue, 29 Nov 2022 13:04:17 +0100 Subject: [PATCH] Add helper class to keep track of the participants in a call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For now only the same signaling messages that were already handled are still handled; in the future it could be extended to handle other messages, like the one sent by the external signaling server when a participant leaves the room (in some cases no participants update message is sent if the participant leaves the call and room at the same time, which causes the participants to still be seen as in call until a new update is received). Signed-off-by: Daniel Calviño Sánchez --- .../talk/call/CallParticipantList.java | 164 +++++ .../call/CallParticipantListNotifier.java | 63 ++ ...lParticipantListExternalSignalingTest.java | 663 ++++++++++++++++++ ...lParticipantListInternalSignalingTest.java | 535 ++++++++++++++ .../talk/call/CallParticipantListTest.java | 61 ++ 5 files changed, 1486 insertions(+) create mode 100644 app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java create mode 100644 app/src/main/java/com/nextcloud/talk/call/CallParticipantListNotifier.java create mode 100644 app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java create mode 100644 app/src/test/java/com/nextcloud/talk/call/CallParticipantListTest.java diff --git a/app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java b/app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java new file mode 100644 index 000000000..6135ab991 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/CallParticipantList.java @@ -0,0 +1,164 @@ +/* + * 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.call; + +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.signaling.SignalingMessageReceiver; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Helper class to keep track of the participants in a call based on the signaling messages. + * + * The CallParticipantList adds a listener for participant list messages as soon as it is created and starts tracking + * the call participants until destroyed. Notifications about the changes can be received by adding an observer to the + * CallParticipantList; note that no sorting is guaranteed on the participants. + */ +public class CallParticipantList { + + public interface Observer { + void onCallParticipantsChanged(Collection joined, Collection updated, + Collection left, Collection unchanged); + void onCallEndedForAll(); + } + + private final SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener = + new SignalingMessageReceiver.ParticipantListMessageListener() { + + private final Map callParticipants = new HashMap<>(); + + @Override + public void onUsersInRoom(List participants) { + processParticipantList(participants); + } + + @Override + public void onParticipantsUpdate(List participants) { + processParticipantList(participants); + } + + private void processParticipantList(List participants) { + Collection joined = new ArrayList<>(); + Collection updated = new ArrayList<>(); + Collection left = new ArrayList<>(); + Collection unchanged = new ArrayList<>(); + + Collection knownCallParticipantsNotFound = new ArrayList<>(callParticipants.values()); + + for (Participant participant : participants) { + String sessionId = participant.getSessionId(); + Participant callParticipant = callParticipants.get(sessionId); + + boolean knownCallParticipant = callParticipant != null; + if (!knownCallParticipant && participant.getInCall() != Participant.InCallFlags.DISCONNECTED) { + callParticipants.put(sessionId, copyParticipant(participant)); + joined.add(copyParticipant(participant)); + } else if (knownCallParticipant && participant.getInCall() == Participant.InCallFlags.DISCONNECTED) { + callParticipants.remove(sessionId); + // No need to copy it, as it will be no longer used. + callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + left.add(callParticipant); + } else if (knownCallParticipant && callParticipant.getInCall() != participant.getInCall()) { + callParticipant.setInCall(participant.getInCall()); + updated.add(copyParticipant(participant)); + } else if (knownCallParticipant) { + unchanged.add(copyParticipant(participant)); + } + + if (knownCallParticipant) { + knownCallParticipantsNotFound.remove(callParticipant); + } + } + + for (Participant callParticipant : knownCallParticipantsNotFound) { + callParticipants.remove(callParticipant.getSessionId()); + // No need to copy it, as it will be no longer used. + callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + left.add(callParticipant); + } + + if (!joined.isEmpty() || !updated.isEmpty() || !left.isEmpty()) { + callParticipantListNotifier.notifyChanged(joined, updated, left, unchanged); + } + } + + @Override + public void onAllParticipantsUpdate(long inCall) { + if (inCall != Participant.InCallFlags.DISCONNECTED) { + // Updating all participants is expected to happen only to disconnect them. + return; + } + + callParticipantListNotifier.notifyCallEndedForAll(); + + Collection joined = new ArrayList<>(); + Collection updated = new ArrayList<>(); + Collection left = new ArrayList<>(callParticipants.size()); + Collection unchanged = new ArrayList<>(); + + for (Participant callParticipant : callParticipants.values()) { + // No need to copy it, as it will be no longer used. + callParticipant.setInCall(Participant.InCallFlags.DISCONNECTED); + left.add(callParticipant); + } + callParticipants.clear(); + + if (!left.isEmpty()) { + callParticipantListNotifier.notifyChanged(joined, updated, left, unchanged); + } + } + + private Participant copyParticipant(Participant participant) { + Participant copiedParticipant = new Participant(); + copiedParticipant.setInCall(participant.getInCall()); + copiedParticipant.setLastPing(participant.getLastPing()); + copiedParticipant.setSessionId(participant.getSessionId()); + copiedParticipant.setType(participant.getType()); + copiedParticipant.setUserId(participant.getUserId()); + + return copiedParticipant; + } + }; + + private final CallParticipantListNotifier callParticipantListNotifier = new CallParticipantListNotifier(); + + private final SignalingMessageReceiver signalingMessageReceiver; + + public CallParticipantList(SignalingMessageReceiver signalingMessageReceiver) { + this.signalingMessageReceiver = signalingMessageReceiver; + this.signalingMessageReceiver.addListener(participantListMessageListener); + } + + public void destroy() { + signalingMessageReceiver.removeListener(participantListMessageListener); + } + + public void addObserver(Observer observer) { + callParticipantListNotifier.addObserver(observer); + } + + public void removeObserver(Observer observer) { + callParticipantListNotifier.removeObserver(observer); + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/CallParticipantListNotifier.java b/app/src/main/java/com/nextcloud/talk/call/CallParticipantListNotifier.java new file mode 100644 index 000000000..afbc893fd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/CallParticipantListNotifier.java @@ -0,0 +1,63 @@ +/* + * 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.call; + +import com.nextcloud.talk.models.json.participants.Participant; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Helper class to register and notify CallParticipantList.Observers. + * + * This class is only meant for internal use by CallParticipantList; listeners must register themselves against + * a CallParticipantList rather than against a CallParticipantListNotifier. + */ +class CallParticipantListNotifier { + + private final Set callParticipantListObservers = new LinkedHashSet<>(); + + public synchronized void addObserver(CallParticipantList.Observer observer) { + if (observer == null) { + throw new IllegalArgumentException("CallParticipantList.Observer can not be null"); + } + + callParticipantListObservers.add(observer); + } + + public synchronized void removeObserver(CallParticipantList.Observer observer) { + callParticipantListObservers.remove(observer); + } + + public synchronized void notifyChanged(Collection joined, Collection updated, + Collection left, Collection unchanged) { + for (CallParticipantList.Observer observer : new ArrayList<>(callParticipantListObservers)) { + observer.onCallParticipantsChanged(joined, updated, left, unchanged); + } + } + + public synchronized void notifyCallEndedForAll() { + for (CallParticipantList.Observer observer : new ArrayList<>(callParticipantListObservers)) { + observer.onCallEndedForAll(); + } + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java new file mode 100644 index 000000000..7d2012207 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListExternalSignalingTest.java @@ -0,0 +1,663 @@ +/* + * 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.call; + +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.signaling.SignalingMessageReceiver; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.InOrder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.DISCONNECTED; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.IN_CALL; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_AUDIO; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_VIDEO; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.GUEST; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.GUEST_MODERATOR; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.MODERATOR; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.OWNER; +import static com.nextcloud.talk.models.json.participants.Participant.ParticipantType.USER; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +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; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class CallParticipantListExternalSignalingTest { + + private static class ParticipantsUpdateParticipantBuilder { + private Participant newUser(long inCall, long lastPing, String sessionId, Participant.ParticipantType type, + String userId) { + Participant participant = new Participant(); + participant.setInCall(inCall); + participant.setLastPing(lastPing); + participant.setSessionId(sessionId); + participant.setType(type); + participant.setUserId(userId); + + return participant; + } + + private Participant newGuest(long inCall, long lastPing, String sessionId, Participant.ParticipantType type) { + Participant participant = new Participant(); + participant.setInCall(inCall); + participant.setLastPing(lastPing); + participant.setSessionId(sessionId); + participant.setType(type); + + return participant; + } + } + + private final ParticipantsUpdateParticipantBuilder builder = new ParticipantsUpdateParticipantBuilder(); + + private CallParticipantList callParticipantList; + private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener; + + private CallParticipantList.Observer mockedCallParticipantListObserver; + + private Collection expectedJoined; + private Collection expectedUpdated; + private Collection expectedLeft; + private Collection expectedUnchanged; + + // The order of the left participants in some tests depends on how they are internally sorted by the map, so the + // list of left participants needs to be checked ignoring the sorting (or, rather, sorting by session ID as in + // expectedLeft). + // Other tests can just relay on the not guaranteed, but known internal sorting of the elements. + private final ArgumentMatcher> matchesExpectedLeftIgnoringOrder = left -> { + Collections.sort(left, Comparator.comparing(Participant::getSessionId)); + return expectedLeft.equals(left); + }; + + @Before + public void setUp() { + SignalingMessageReceiver mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class); + + callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver); + + mockedCallParticipantListObserver = mock(CallParticipantList.Observer.class); + + // Get internal ParticipantListMessageListener from callParticipantList set in the + // mockedSignalingMessageReceiver. + ArgumentCaptor participantListMessageListenerArgumentCaptor = + ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class); + + verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture()); + + participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue(); + + expectedJoined = new ArrayList<>(); + expectedUpdated = new ArrayList<>(); + expectedLeft = new ArrayList<>(); + expectedUnchanged = new ArrayList<>(); + } + + @Test + public void testParticipantsUpdateJoinRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateJoinRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", USER, "theUserId4")); + participants.add(builder.newUser(DISCONNECTED, 5, "theSessionId5", OWNER, "theUserId5")); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateJoinRoomThenJoinCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateJoinRoomThenJoinCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateJoinRoomAndCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateJoinRoomAndCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateJoinRoomAndCallRepeated() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + participantListMessageListener.onParticipantsUpdate(participants); + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateChangeCallFlags() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateChangeCallFlagsSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", USER, "theUserId4")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedUpdated.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedUpdated.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", USER, "theUserId4")); + expectedUnchanged.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2", GUEST)); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateChangeLastPing() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", MODERATOR, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateChangeLastPingSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 108, "theSessionId2", GUEST)); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 815, "theSessionId3", USER, "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateChangeParticipantType() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", USER, "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateChangeParticipantTypeeSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", USER, "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2", GUEST_MODERATOR)); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", MODERATOR, "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateLeaveCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateLeaveCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateLeaveCallThenLeaveRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateLeaveCallThenLeaveRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testParticipantsUpdateLeaveCallAndRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testParticipantsUpdateLeaveCallAndRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + + verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } + + @Test + public void testParticipantsUpdateSeveralEventsSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + participants.add(builder.newUser(IN_CALL, 5, "theSessionId5", OWNER, "theUserId5")); + // theSessionId6 has not joined yet. + participants.add(builder.newGuest(IN_CALL | WITH_VIDEO, 7, "theSessionId7", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 8, "theSessionId8", USER, "theUserId8")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 9, "theSessionId9", MODERATOR, "theUserId9")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + // theSessionId1 is gone. + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", OWNER, "theUserId5")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6", GUEST)); + participants.add(builder.newGuest(IN_CALL, 7, "theSessionId7", GUEST)); + participants.add(builder.newUser(IN_CALL, 8, "theSessionId8", USER, "theUserId8")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", USER, "theUserId9")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6", GUEST)); + expectedJoined.add(builder.newUser(IN_CALL, 8, "theSessionId8", USER, "theUserId8")); + expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", OWNER, "theUserId5")); + expectedUpdated.add(builder.newGuest(IN_CALL, 7, "theSessionId7", GUEST)); + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2", GUEST)); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", USER, "theUserId4")); + // Last ping and participant type are not seen as changed, even if they did. + expectedUnchanged.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", USER, "theUserId9")); + + verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } + + @Test + public void testAllParticipantsUpdateDisconnected() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + + InOrder inOrder = inOrder(mockedCallParticipantListObserver); + + inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll(); + inOrder.verify(mockedCallParticipantListObserver).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testAllParticipantsUpdateDisconnectedWithSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + participants.add(builder.newUser(DISCONNECTED, 2, "theSessionId2", USER, "theUserId2")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", USER, "theUserId3")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 4, "theSessionId4", GUEST)); + + participantListMessageListener.onParticipantsUpdate(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", MODERATOR, "theUserId1")); + expectedLeft.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", USER, "theUserId3")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 4, "theSessionId4", GUEST)); + + InOrder inOrder = inOrder(mockedCallParticipantListObserver); + + inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll(); + inOrder.verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } + + @Test + public void testAllParticipantsUpdateDisconnectedNoOneInCall() { + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED); + + InOrder inOrder = inOrder(mockedCallParticipantListObserver); + + inOrder.verify(mockedCallParticipantListObserver).onCallEndedForAll(); + verifyNoMoreInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testAllParticipantsUpdateDisconnectedThenJoinCallAgain() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + participantListMessageListener.onAllParticipantsUpdate(DISCONNECTED); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + + participantListMessageListener.onParticipantsUpdate(participants); + + expectedJoined.add(builder.newUser(IN_CALL, 1, "theSessionId1", MODERATOR, "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java new file mode 100644 index 000000000..fb393b10f --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListInternalSignalingTest.java @@ -0,0 +1,535 @@ +/* + * 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.call; + +import com.nextcloud.talk.models.json.participants.Participant; +import com.nextcloud.talk.signaling.SignalingMessageReceiver; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.DISCONNECTED; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.IN_CALL; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_AUDIO; +import static com.nextcloud.talk.models.json.participants.Participant.InCallFlags.WITH_VIDEO; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +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 CallParticipantListInternalSignalingTest { + + private static class UsersInRoomParticipantBuilder { + private Participant newUser(long inCall, long lastPing, String sessionId, String userId) { + Participant participant = new Participant(); + participant.setInCall(inCall); + participant.setLastPing(lastPing); + participant.setSessionId(sessionId); + participant.setUserId(userId); + + return participant; + } + + private Participant newGuest(long inCall, long lastPing, String sessionId) { + Participant participant = new Participant(); + participant.setInCall(inCall); + participant.setLastPing(lastPing); + participant.setSessionId(sessionId); + + return participant; + } + } + + private final UsersInRoomParticipantBuilder builder = new UsersInRoomParticipantBuilder(); + + private CallParticipantList callParticipantList; + private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener; + + private CallParticipantList.Observer mockedCallParticipantListObserver; + + private Collection expectedJoined; + private Collection expectedUpdated; + private Collection expectedLeft; + private Collection expectedUnchanged; + + // The order of the left participants in some tests depends on how they are internally sorted by the map, so the + // list of left participants needs to be checked ignoring the sorting (or, rather, sorting by session ID as in + // expectedLeft). + // Other tests can just relay on the not guaranteed, but known internal sorting of the elements. + private final ArgumentMatcher> matchesExpectedLeftIgnoringOrder = left -> { + Collections.sort(left, Comparator.comparing(Participant::getSessionId)); + return expectedLeft.equals(left); + }; + + @Before + public void setUp() { + SignalingMessageReceiver mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class); + + callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver); + + mockedCallParticipantListObserver = mock(CallParticipantList.Observer.class); + + // Get internal ParticipantListMessageListener from callParticipantList set in the + // mockedSignalingMessageReceiver. + ArgumentCaptor participantListMessageListenerArgumentCaptor = + ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class); + + verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture()); + + participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue(); + + expectedJoined = new ArrayList<>(); + expectedUpdated = new ArrayList<>(); + expectedLeft = new ArrayList<>(); + expectedUnchanged = new ArrayList<>(); + } + + @Test + public void testUsersInRoomJoinRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomJoinRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", "theUserId4")); + participants.add(builder.newUser(DISCONNECTED, 5, "theSessionId5", "theUserId5")); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomJoinRoomThenJoinCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomJoinRoomThenJoinCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(DISCONNECTED, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomJoinRoomAndCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomJoinRoomAndCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + expectedJoined.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomJoinRoomAndCallRepeated() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + participantListMessageListener.onUsersInRoom(participants); + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomChangeCallFlags() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomChangeCallFlagsSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", "theUserId4")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + expectedUpdated.add(builder.newUser(IN_CALL, 1, "theSessionId1", "theUserId1")); + expectedUpdated.add(builder.newUser(IN_CALL | WITH_VIDEO, 4, "theSessionId4", "theUserId4")); + expectedUnchanged.add(builder.newGuest(IN_CALL | WITH_AUDIO | WITH_VIDEO, 2, "theSessionId2")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomChangeLastPing() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", "theUserId1")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomChangeLastPingSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 2, "theSessionId2")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 3, "theSessionId3", "theUserId3")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 108, "theSessionId2")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 815, "theSessionId3", "theUserId3")); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomLeaveCall() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomLeaveCallSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomLeaveCallThenLeaveRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomLeaveCallThenLeaveRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + verifyNoInteractions(mockedCallParticipantListObserver); + } + + @Test + public void testUsersInRoomLeaveCallAndRoom() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + + participantListMessageListener.onUsersInRoom(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + + verify(mockedCallParticipantListObserver, only()).onCallParticipantsChanged(expectedJoined, expectedUpdated, + expectedLeft, expectedUnchanged); + } + + @Test + public void testUsersInRoomLeaveCallAndRoomSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + + verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } + + @Test + public void testUsersInRoomSeveralEventsSeveralParticipants() { + List participants = new ArrayList<>(); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 1, "theSessionId1", "theUserId1")); + participants.add(builder.newGuest(IN_CALL, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + participants.add(builder.newUser(IN_CALL, 5, "theSessionId5", "theUserId5")); + // theSessionId6 has not joined yet. + participants.add(builder.newGuest(IN_CALL | WITH_VIDEO, 7, "theSessionId7")); + participants.add(builder.newUser(DISCONNECTED, 8, "theSessionId8", "theUserId8")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 9, "theSessionId9", "theUserId9")); + + participantListMessageListener.onUsersInRoom(participants); + + callParticipantList.addObserver(mockedCallParticipantListObserver); + + participants = new ArrayList<>(); + // theSessionId1 is gone. + participants.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + participants.add(builder.newUser(DISCONNECTED, 3, "theSessionId3", "theUserId3")); + participants.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", "theUserId5")); + participants.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6")); + participants.add(builder.newGuest(IN_CALL, 7, "theSessionId7")); + participants.add(builder.newUser(IN_CALL, 8, "theSessionId8", "theUserId8")); + participants.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", "theUserId9")); + + participantListMessageListener.onUsersInRoom(participants); + + expectedJoined.add(builder.newGuest(IN_CALL | WITH_AUDIO, 6, "theSessionId6")); + expectedJoined.add(builder.newUser(IN_CALL, 8, "theSessionId8", "theUserId8")); + expectedUpdated.add(builder.newUser(IN_CALL | WITH_AUDIO | WITH_VIDEO, 5, "theSessionId5", "theUserId5")); + expectedUpdated.add(builder.newGuest(IN_CALL, 7, "theSessionId7")); + expectedLeft.add(builder.newUser(DISCONNECTED, 1, "theSessionId1", "theUserId1")); + expectedLeft.add(builder.newGuest(DISCONNECTED, 2, "theSessionId2")); + expectedUnchanged.add(builder.newUser(IN_CALL, 4, "theSessionId4", "theUserId4")); + // Last ping is not seen as changed, even if it did. + expectedUnchanged.add(builder.newUser(IN_CALL | WITH_AUDIO, 42, "theSessionId9", "theUserId9")); + + verify(mockedCallParticipantListObserver).onCallParticipantsChanged(eq(expectedJoined), eq(expectedUpdated), + argThat(matchesExpectedLeftIgnoringOrder), eq(expectedUnchanged)); + } +} diff --git a/app/src/test/java/com/nextcloud/talk/call/CallParticipantListTest.java b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListTest.java new file mode 100644 index 000000000..7fed0f6d8 --- /dev/null +++ b/app/src/test/java/com/nextcloud/talk/call/CallParticipantListTest.java @@ -0,0 +1,61 @@ +/* + * 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.call; + +import com.nextcloud.talk.signaling.SignalingMessageReceiver; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +public class CallParticipantListTest { + + private SignalingMessageReceiver mockedSignalingMessageReceiver; + + private CallParticipantList callParticipantList; + private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener; + + @Before + public void setUp() { + mockedSignalingMessageReceiver = mock(SignalingMessageReceiver.class); + + callParticipantList = new CallParticipantList(mockedSignalingMessageReceiver); + + // Get internal ParticipantListMessageListener from callParticipantList set in the + // mockedSignalingMessageReceiver. + ArgumentCaptor participantListMessageListenerArgumentCaptor = + ArgumentCaptor.forClass(SignalingMessageReceiver.ParticipantListMessageListener.class); + + verify(mockedSignalingMessageReceiver).addListener(participantListMessageListenerArgumentCaptor.capture()); + + participantListMessageListener = participantListMessageListenerArgumentCaptor.getValue(); + } + + @Test + public void testDestroy() { + callParticipantList.destroy(); + + verify(mockedSignalingMessageReceiver).removeListener(participantListMessageListener); + verifyNoMoreInteractions(mockedSignalingMessageReceiver); + } +}