Distinguish between primary and secondary devices in participants list.

This commit is contained in:
Alex Hart 2021-01-13 11:37:29 -04:00 committed by Greyson Parrelli
parent 2d20ceea01
commit 0ccc7e3c06
10 changed files with 132 additions and 104 deletions

View file

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.webrtc; package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Collectors; import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
@ -21,19 +22,19 @@ import java.util.Set;
*/ */
public final class CallParticipantListUpdate { public final class CallParticipantListUpdate {
private final Set<Holder> added; private final Set<Wrapper> added;
private final Set<Holder> removed; private final Set<Wrapper> removed;
CallParticipantListUpdate(@NonNull Set<Holder> added, @NonNull Set<Holder> removed) { CallParticipantListUpdate(@NonNull Set<Wrapper> added, @NonNull Set<Wrapper> removed) {
this.added = added; this.added = added;
this.removed = removed; this.removed = removed;
} }
public @NonNull Set<Holder> getAdded() { public @NonNull Set<Wrapper> getAdded() {
return added; return added;
} }
public @NonNull Set<Holder> getRemoved() { public @NonNull Set<Wrapper> getRemoved() {
return removed; return removed;
} }
@ -68,66 +69,47 @@ public final class CallParticipantListUpdate {
public static @NonNull CallParticipantListUpdate computeDeltaUpdate(@NonNull List<CallParticipant> oldList, public static @NonNull CallParticipantListUpdate computeDeltaUpdate(@NonNull List<CallParticipant> oldList,
@NonNull List<CallParticipant> newList) @NonNull List<CallParticipant> newList)
{ {
Set<CallParticipantId> primaries = getPrimaries(oldList, newList); Set<CallParticipantListUpdate.Wrapper> oldParticipants = Stream.of(oldList)
Set<CallParticipantListUpdate.Holder> oldParticipants = Stream.of(oldList)
.filter(p -> p.getCallParticipantId().getDemuxId() != CallParticipantId.DEFAULT_ID) .filter(p -> p.getCallParticipantId().getDemuxId() != CallParticipantId.DEFAULT_ID)
.map(p -> createHolder(p, primaries.contains(p.getCallParticipantId()))) .map(CallParticipantListUpdate::createWrapper)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Set<CallParticipantListUpdate.Holder> newParticipants = Stream.of(newList) Set<CallParticipantListUpdate.Wrapper> newParticipants = Stream.of(newList)
.filter(p -> p.getCallParticipantId().getDemuxId() != CallParticipantId.DEFAULT_ID) .filter(p -> p.getCallParticipantId().getDemuxId() != CallParticipantId.DEFAULT_ID)
.map(p -> createHolder(p, primaries.contains(p.getCallParticipantId()))) .map(CallParticipantListUpdate::createWrapper)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Set<CallParticipantListUpdate.Holder> added = SetUtil.difference(newParticipants, oldParticipants); Set<CallParticipantListUpdate.Wrapper> added = SetUtil.difference(newParticipants, oldParticipants);
Set<CallParticipantListUpdate.Holder> removed = SetUtil.difference(oldParticipants, newParticipants); Set<CallParticipantListUpdate.Wrapper> removed = SetUtil.difference(oldParticipants, newParticipants);
return new CallParticipantListUpdate(added, removed); return new CallParticipantListUpdate(added, removed);
} }
static Holder createHolder(@NonNull CallParticipant callParticipant, boolean isPrimary) { @VisibleForTesting
return new Holder(callParticipant.getCallParticipantId(), callParticipant.getRecipient(), isPrimary); static Wrapper createWrapper(@NonNull CallParticipant callParticipant) {
return new Wrapper(callParticipant);
} }
private static @NonNull Set<CallParticipantId> getPrimaries(@NonNull List<CallParticipant> oldList, @NonNull List<CallParticipant> newList) { static final class Wrapper {
return Stream.concat(Stream.of(oldList), Stream.of(newList)) private final CallParticipant callParticipant;
.map(CallParticipant::getCallParticipantId)
.distinctBy(CallParticipantId::getRecipientId) private Wrapper(@NonNull CallParticipant callParticipant) {
.collect(Collectors.toSet()); this.callParticipant = callParticipant;
} }
static final class Holder { public @NonNull CallParticipant getCallParticipant() {
private final CallParticipantId callParticipantId; return callParticipant;
private final Recipient recipient;
private final boolean isPrimary;
private Holder(@NonNull CallParticipantId callParticipantId, @NonNull Recipient recipient, boolean isPrimary) {
this.callParticipantId = callParticipantId;
this.recipient = recipient;
this.isPrimary = isPrimary;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
/**
* Denotes whether this was the first detected instance of this recipient when generating an update. See
* {@link CallParticipantListUpdate#computeDeltaUpdate(List, List)}
*/
public boolean isPrimary() {
return isPrimary;
} }
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
Holder holder = (Holder) o; Wrapper wrapper = (Wrapper) o;
return callParticipantId.equals(holder.callParticipantId); return callParticipant.getCallParticipantId().equals(wrapper.callParticipant.getCallParticipantId());
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(callParticipantId); return Objects.hash(callParticipant.getCallParticipantId());
} }
} }
} }

View file

@ -31,8 +31,8 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
private final AvatarImageView avatarImageView; private final AvatarImageView avatarImageView;
private final TextView descriptionTextView; private final TextView descriptionTextView;
private final Set<CallParticipantListUpdate.Holder> pendingAdditions = new HashSet<>(); private final Set<CallParticipantListUpdate.Wrapper> pendingAdditions = new HashSet<>();
private final Set<CallParticipantListUpdate.Holder> pendingRemovals = new HashSet<>(); private final Set<CallParticipantListUpdate.Wrapper> pendingRemovals = new HashSet<>();
private boolean isEnabled = true; private boolean isEnabled = true;
@ -112,16 +112,16 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
avatarImageView.setVisibility(recipient == null ? View.GONE : View.VISIBLE); avatarImageView.setVisibility(recipient == null ? View.GONE : View.VISIBLE);
} }
private void setDescription(@NonNull Set<CallParticipantListUpdate.Holder> holders, boolean isAdded) { private void setDescription(@NonNull Set<CallParticipantListUpdate.Wrapper> wrappers, boolean isAdded) {
if (holders.isEmpty()) { if (wrappers.isEmpty()) {
descriptionTextView.setText(""); descriptionTextView.setText("");
} else { } else {
setDescriptionForRecipients(holders, isAdded); setDescriptionForRecipients(wrappers, isAdded);
} }
} }
private void setDescriptionForRecipients(@NonNull Set<CallParticipantListUpdate.Holder> recipients, boolean isAdded) { private void setDescriptionForRecipients(@NonNull Set<CallParticipantListUpdate.Wrapper> recipients, boolean isAdded) {
Iterator<CallParticipantListUpdate.Holder> iterator = recipients.iterator(); Iterator<CallParticipantListUpdate.Wrapper> iterator = recipients.iterator();
Context context = getContentView().getContext(); Context context = getContentView().getContext();
String description; String description;
@ -144,22 +144,14 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
descriptionTextView.setText(description); descriptionTextView.setText(description);
} }
private @NonNull Recipient getNextRecipient(@NonNull Iterator<CallParticipantListUpdate.Holder> holderIterator) { private @NonNull Recipient getNextRecipient(@NonNull Iterator<CallParticipantListUpdate.Wrapper> wrapperIterator) {
return holderIterator.next().getRecipient(); return wrapperIterator.next().getCallParticipant().getRecipient();
} }
private @NonNull String getNextDisplayName(@NonNull Iterator<CallParticipantListUpdate.Holder> holderIterator) { private @NonNull String getNextDisplayName(@NonNull Iterator<CallParticipantListUpdate.Wrapper> wrapperIterator) {
CallParticipantListUpdate.Holder holder = holderIterator.next(); CallParticipantListUpdate.Wrapper wrapper = wrapperIterator.next();
Recipient recipient = holder.getRecipient();
if (recipient.isSelf()) { return wrapper.getCallParticipant().getRecipientDisplayName(getContentView().getContext());
return getContentView().getContext().getString(R.string.CallParticipantsListUpdatePopupWindow__you_on_another_device);
} else if (holder.isPrimary()) {
return recipient.getDisplayName(getContentView().getContext());
} else {
return getContentView().getContext().getString(R.string.CallParticipantsListUpdatePopupWindow__s_on_another_device,
recipient.getDisplayName(getContentView().getContext()));
}
} }
private static @StringRes int getOneMemberDescriptionResourceId(boolean isAdded) { private static @StringRes int getOneMemberDescriptionResourceId(boolean isAdded) {

View file

@ -25,8 +25,7 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
@Override @Override
public @NonNull String getName(@NonNull Context context) { public @NonNull String getName(@NonNull Context context) {
return callParticipant.getRecipient().isSelf() ? context.getString(R.string.GroupMembersDialog_you) return callParticipant.getRecipientDisplayName(context);
: super.getName(context);
} }
public int getVideoMutedVisibility() { public int getVideoMutedVisibility() {

View file

@ -1,8 +1,11 @@
package org.thoughtcrime.securesms.events; package org.thoughtcrime.securesms.events;
import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.CameraState;
@ -12,7 +15,7 @@ import java.util.Objects;
public final class CallParticipant { public final class CallParticipant {
public static final CallParticipant EMPTY = createRemote(new CallParticipantId(Recipient.UNKNOWN), Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false, false, 0, true, 0); public static final CallParticipant EMPTY = createRemote(new CallParticipantId(Recipient.UNKNOWN), Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false, false, 0, true, 0, DeviceOrdinal.PRIMARY);
private final @NonNull CallParticipantId callParticipantId; private final @NonNull CallParticipantId callParticipantId;
private final @NonNull CameraState cameraState; private final @NonNull CameraState cameraState;
@ -24,6 +27,7 @@ public final class CallParticipant {
private final long lastSpoke; private final long lastSpoke;
private final boolean mediaKeysReceived; private final boolean mediaKeysReceived;
private final long addedToCallTime; private final long addedToCallTime;
private final @NonNull DeviceOrdinal deviceOrdinal;
public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState, public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState,
@NonNull BroadcastVideoSink renderer, @NonNull BroadcastVideoSink renderer,
@ -38,7 +42,8 @@ public final class CallParticipant {
microphoneEnabled, microphoneEnabled,
0, 0,
true, true,
0); 0,
DeviceOrdinal.PRIMARY);
} }
public static @NonNull CallParticipant createRemote(@NonNull CallParticipantId callParticipantId, public static @NonNull CallParticipant createRemote(@NonNull CallParticipantId callParticipantId,
@ -49,9 +54,10 @@ public final class CallParticipant {
boolean videoEnabled, boolean videoEnabled,
long lastSpoke, long lastSpoke,
boolean mediaKeysReceived, boolean mediaKeysReceived,
long addedToCallTime) long addedToCallTime,
@NonNull DeviceOrdinal deviceOrdinal)
{ {
return new CallParticipant(callParticipantId, recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, audioEnabled, lastSpoke, mediaKeysReceived, addedToCallTime); return new CallParticipant(callParticipantId, recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, audioEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal);
} }
private CallParticipant(@NonNull CallParticipantId callParticipantId, private CallParticipant(@NonNull CallParticipantId callParticipantId,
@ -63,7 +69,8 @@ public final class CallParticipant {
boolean microphoneEnabled, boolean microphoneEnabled,
long lastSpoke, long lastSpoke,
boolean mediaKeysReceived, boolean mediaKeysReceived,
long addedToCallTime) long addedToCallTime,
@NonNull DeviceOrdinal deviceOrdinal)
{ {
this.callParticipantId = callParticipantId; this.callParticipantId = callParticipantId;
this.recipient = recipient; this.recipient = recipient;
@ -75,14 +82,15 @@ public final class CallParticipant {
this.lastSpoke = lastSpoke; this.lastSpoke = lastSpoke;
this.mediaKeysReceived = mediaKeysReceived; this.mediaKeysReceived = mediaKeysReceived;
this.addedToCallTime = addedToCallTime; this.addedToCallTime = addedToCallTime;
this.deviceOrdinal = deviceOrdinal;
} }
public @NonNull CallParticipant withIdentityKey(@NonNull IdentityKey identityKey) { public @NonNull CallParticipant withIdentityKey(@NonNull IdentityKey identityKey) {
return new CallParticipant(callParticipantId, recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime); return new CallParticipant(callParticipantId, recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal);
} }
public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) { public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) {
return new CallParticipant(callParticipantId, recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime); return new CallParticipant(callParticipantId, recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal);
} }
public @NonNull CallParticipantId getCallParticipantId() { public @NonNull CallParticipantId getCallParticipantId() {
@ -93,6 +101,18 @@ public final class CallParticipant {
return recipient; return recipient;
} }
public @NonNull String getRecipientDisplayName(@NonNull Context context) {
if (recipient.isSelf() && isPrimary()) {
return context.getString(R.string.CallParticipant__you);
} else if (recipient.isSelf()) {
return context.getString(R.string.CallParticipant__you_on_another_device);
} else if (isPrimary()) {
return recipient.getDisplayName(context);
} else {
return context.getString(R.string.CallParticipant__s_on_another_device, recipient.getDisplayName(context));
}
}
public @Nullable IdentityKey getIdentityKey() { public @Nullable IdentityKey getIdentityKey() {
return identityKey; return identityKey;
} }
@ -136,6 +156,10 @@ public final class CallParticipant {
return addedToCallTime; return addedToCallTime;
} }
public boolean isPrimary() {
return deviceOrdinal == DeviceOrdinal.PRIMARY;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@ -172,4 +196,9 @@ public final class CallParticipant {
", addedToCallTime=" + addedToCallTime + ", addedToCallTime=" + addedToCallTime +
'}'; '}';
} }
public enum DeviceOrdinal {
PRIMARY,
SECONDARY
}
} }

View file

@ -50,7 +50,8 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
false, false,
0, 0,
true, true,
0 0,
CallParticipant.DeviceOrdinal.PRIMARY
)) ))
.build(); .build();
@ -93,7 +94,8 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
false, false,
0, 0,
true, true,
0 0,
CallParticipant.DeviceOrdinal.PRIMARY
)) ))
.build(); .build();
} }

View file

@ -30,8 +30,11 @@ import org.whispersystems.signalservice.api.messages.calls.OpaqueMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Base group call action processor that handles general callbacks around call members * Base group call action processor that handles general callbacks around call members
@ -74,8 +77,16 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
.changeCallInfoState() .changeCallInfoState()
.clearParticipantMap(); .clearParticipantMap();
List<GroupCall.RemoteDeviceState> remoteDeviceStates = new ArrayList<>(remoteDevices.size());
for (int i = 0; i < remoteDevices.size(); i++) { for (int i = 0; i < remoteDevices.size(); i++) {
GroupCall.RemoteDeviceState device = remoteDevices.get(remoteDevices.keyAt(i)); remoteDeviceStates.add(remoteDevices.get(remoteDevices.keyAt(i)));
}
Collections.sort(remoteDeviceStates, (a, b) -> Long.compare(a.getAddedTime(), b.getAddedTime()));
Set<Recipient> seen = new HashSet<>();
seen.add(Recipient.self());
for (GroupCall.RemoteDeviceState device : remoteDeviceStates) {
Recipient recipient = Recipient.externalPush(context, device.getUserId(), null, false); Recipient recipient = Recipient.externalPush(context, device.getUserId(), null, false);
CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId()); CallParticipantId callParticipantId = new CallParticipantId(device.getDemuxId(), recipient.getId());
CallParticipant callParticipant = participants.get(callParticipantId); CallParticipant callParticipant = participants.get(callParticipantId);
@ -99,7 +110,11 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
Boolean.FALSE.equals(device.getVideoMuted()), Boolean.FALSE.equals(device.getVideoMuted()),
device.getSpeakerTime(), device.getSpeakerTime(),
device.getMediaKeysReceived(), device.getMediaKeysReceived(),
device.getAddedTime())); device.getAddedTime(),
seen.contains(recipient) ? CallParticipant.DeviceOrdinal.SECONDARY
: CallParticipant.DeviceOrdinal.PRIMARY));
seen.add(recipient);
} }
builder.remoteDevicesCount(remoteDevices.size()); builder.remoteDevicesCount(remoteDevices.size());

View file

@ -122,7 +122,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
.clearParticipantMap(); .clearParticipantMap();
for (Recipient recipient : callParticipants) { for (Recipient recipient : callParticipants) {
builder.putParticipant(recipient, CallParticipant.createRemote(new CallParticipantId(recipient), recipient, null, new BroadcastVideoSink(null), true, true, 0, false, 0)); builder.putParticipant(recipient, CallParticipant.createRemote(new CallParticipantId(recipient), recipient, null, new BroadcastVideoSink(null), true, true, 0, false, 0, CallParticipant.DeviceOrdinal.PRIMARY));
} }
return builder.build(); return builder.build();

View file

@ -3040,8 +3040,10 @@
<string name="CallParticipantsListUpdatePopupWindow__s_and_s_left">%1$s and %2$s left</string> <string name="CallParticipantsListUpdatePopupWindow__s_and_s_left">%1$s and %2$s left</string>
<string name="CallParticipantsListUpdatePopupWindow__s_s_and_s_left">%1$s, %2$s and %3$s left</string> <string name="CallParticipantsListUpdatePopupWindow__s_s_and_s_left">%1$s, %2$s and %3$s left</string>
<string name="CallParticipantsListUpdatePopupWindow__s_s_and_d_others_left">%1$s, %2$s and %3$d others left</string> <string name="CallParticipantsListUpdatePopupWindow__s_s_and_d_others_left">%1$s, %2$s and %3$d others left</string>
<string name="CallParticipantsListUpdatePopupWindow__you_on_another_device">You (on another device)</string>
<string name="CallParticipantsListUpdatePopupWindow__s_on_another_device">%1$s (on another device)</string> <string name="CallParticipant__you">You</string>
<string name="CallParticipant__you_on_another_device">You (on another device)</string>
<string name="CallParticipant__s_on_another_device">%1$s (on another device)</string>
<!-- DeleteAccountFragment --> <!-- DeleteAccountFragment -->
<string name="DeleteAccountFragment__deleting_your_account_will">Deleting your account will:</string> <string name="DeleteAccountFragment__deleting_your_account_will">Deleting your account will:</string>

View file

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.components.webrtc; package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import org.hamcrest.Matchers; import org.hamcrest.Matchers;
@ -27,8 +29,8 @@ public class CallParticipantListUpdateTest {
@Test @Test
public void givenEmptySets_thenExpectNoChanges() { public void givenEmptySets_thenExpectNoChanges() {
// GIVEN // GIVEN
Set<CallParticipantListUpdate.Holder> added = Collections.emptySet(); Set<CallParticipantListUpdate.Wrapper> added = Collections.emptySet();
Set<CallParticipantListUpdate.Holder> removed = Collections.emptySet(); Set<CallParticipantListUpdate.Wrapper> removed = Collections.emptySet();
CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed); CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed);
// THEN // THEN
@ -39,8 +41,8 @@ public class CallParticipantListUpdateTest {
@Test @Test
public void givenOneEmptySet_thenExpectMultipleChanges() { public void givenOneEmptySet_thenExpectMultipleChanges() {
// GIVEN // GIVEN
Set<CallParticipantListUpdate.Holder> added = new HashSet<>(Arrays.asList(createHolders(1, 2, 3))); Set<CallParticipantListUpdate.Wrapper> added = new HashSet<>(Arrays.asList(createWrappers(1, 2, 3)));
Set<CallParticipantListUpdate.Holder> removed = Collections.emptySet(); Set<CallParticipantListUpdate.Wrapper> removed = Collections.emptySet();
CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed); CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed);
// THEN // THEN
@ -51,8 +53,8 @@ public class CallParticipantListUpdateTest {
@Test @Test
public void givenNoEmptySets_thenExpectMultipleChanges() { public void givenNoEmptySets_thenExpectMultipleChanges() {
// GIVEN // GIVEN
Set<CallParticipantListUpdate.Holder> added = new HashSet<>(Arrays.asList(createHolders(1, 2, 3))); Set<CallParticipantListUpdate.Wrapper> added = new HashSet<>(Arrays.asList(createWrappers(1, 2, 3)));
Set<CallParticipantListUpdate.Holder> removed = new HashSet<>(Arrays.asList(createHolders(4, 5, 6))); Set<CallParticipantListUpdate.Wrapper> removed = new HashSet<>(Arrays.asList(createWrappers(4, 5, 6)));
CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed); CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed);
// THEN // THEN
@ -63,8 +65,8 @@ public class CallParticipantListUpdateTest {
@Test @Test
public void givenOneSetWithSingleItemAndAnEmptySet_thenExpectSingleChange() { public void givenOneSetWithSingleItemAndAnEmptySet_thenExpectSingleChange() {
// GIVEN // GIVEN
Set<CallParticipantListUpdate.Holder> added = new HashSet<>(Arrays.asList(createHolders(1))); Set<CallParticipantListUpdate.Wrapper> added = new HashSet<>(Arrays.asList(createWrappers(1)));
Set<CallParticipantListUpdate.Holder> removed = Collections.emptySet(); Set<CallParticipantListUpdate.Wrapper> removed = Collections.emptySet();
CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed); CallParticipantListUpdate update = new CallParticipantListUpdate(added, removed);
// THEN // THEN
@ -83,7 +85,7 @@ public class CallParticipantListUpdateTest {
// THEN // THEN
assertFalse(update.hasNoChanges()); assertFalse(update.hasNoChanges());
assertTrue(update.getRemoved().isEmpty()); assertTrue(update.getRemoved().isEmpty());
assertThat(update.getAdded(), Matchers.containsInAnyOrder(createHolders(1, 2, 3, 4, 5))); assertThat(update.getAdded(), Matchers.containsInAnyOrder(createWrappers(1, 2, 3, 4, 5)));
} }
@Test @Test
@ -121,8 +123,8 @@ public class CallParticipantListUpdateTest {
// THEN // THEN
assertFalse(update.hasNoChanges()); assertFalse(update.hasNoChanges());
assertThat(update.getAdded(), Matchers.containsInAnyOrder(createHolders(6))); assertThat(update.getAdded(), Matchers.containsInAnyOrder(createWrappers(6)));
assertThat(update.getRemoved(), Matchers.containsInAnyOrder(createHolders(1))); assertThat(update.getRemoved(), Matchers.containsInAnyOrder(createWrappers(1)));
} }
@Test @Test
@ -134,17 +136,18 @@ public class CallParticipantListUpdateTest {
CallParticipantListUpdate update = CallParticipantListUpdate.computeDeltaUpdate(Collections.emptyList(), list); CallParticipantListUpdate update = CallParticipantListUpdate.computeDeltaUpdate(Collections.emptyList(), list);
// THEN // THEN
List<Boolean> isPrimaryList = Stream.of(update.getAdded()).map(CallParticipantListUpdate.Holder::isPrimary).toList(); List<Boolean> isPrimaryList = Stream.of(update.getAdded()).map(wrapper -> wrapper.getCallParticipant().isPrimary()).toList();
assertThat(isPrimaryList, Matchers.containsInAnyOrder(true, false, false)); assertThat(isPrimaryList, Matchers.containsInAnyOrder(true, false, false));
} }
static CallParticipantListUpdate.Holder[] createHolders(long ... recipientIds) { static CallParticipantListUpdate.Wrapper[] createWrappers(long ... recipientIds) {
CallParticipantListUpdate.Holder[] ids = new CallParticipantListUpdate.Holder[recipientIds.length]; CallParticipantListUpdate.Wrapper[] ids = new CallParticipantListUpdate.Wrapper[recipientIds.length];
Set<Long> primaries = new HashSet<>();
for (int i = 0; i < recipientIds.length; i++) { for (int i = 0; i < recipientIds.length; i++) {
CallParticipant participant = createParticipant(recipientIds[i], recipientIds[i]); CallParticipant participant = createParticipant(recipientIds[i], recipientIds[i], primaries.contains(recipientIds[i]) ? CallParticipant.DeviceOrdinal.SECONDARY : CallParticipant.DeviceOrdinal.PRIMARY);
ids[i] = CallParticipantListUpdate.createHolder(participant, true); ids[i] = CallParticipantListUpdate.createWrapper(participant);
} }
return ids; return ids;
@ -162,17 +165,20 @@ public class CallParticipantListUpdateTest {
private static List<CallParticipant> createParticipants(long[] recipientIds, long[] placeholderIds) { private static List<CallParticipant> createParticipants(long[] recipientIds, long[] placeholderIds) {
List<CallParticipant> participants = new ArrayList<>(recipientIds.length); List<CallParticipant> participants = new ArrayList<>(recipientIds.length);
Set<Long> primaries = new HashSet<>();
for (int i = 0; i < recipientIds.length; i++) { for (int i = 0; i < recipientIds.length; i++) {
participants.add(createParticipant(recipientIds[i], placeholderIds[i])); participants.add(createParticipant(recipientIds[i], placeholderIds[i], primaries.contains(recipientIds[i]) ? CallParticipant.DeviceOrdinal.SECONDARY : CallParticipant.DeviceOrdinal.PRIMARY));
primaries.add(recipientIds[i]);
} }
return participants; return participants;
} }
private static CallParticipant createParticipant(long recipientId, long deMuxId) { private static CallParticipant createParticipant(long recipientId, long deMuxId, @NonNull CallParticipant.DeviceOrdinal deviceOrdinal) {
Recipient recipient = new Recipient(RecipientId.from(recipientId), mock(RecipientDetails.class), true); Recipient recipient = new Recipient(RecipientId.from(recipientId), mock(RecipientDetails.class), true);
return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(null), false, false, -1, false, 0); return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(null), false, false, -1, false, 0, deviceOrdinal);
} }
} }

View file

@ -213,6 +213,7 @@ public class ParticipantCollectionTest {
false, false,
lastSpoke, lastSpoke,
false, false,
added); added,
CallParticipant.DeviceOrdinal.PRIMARY);
} }
} }