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