From 2dcc7d284f8406117c03a48f64a3595e2534a60d Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Sat, 5 Dec 2020 20:55:52 -0500 Subject: [PATCH] Update group membership for a group call when it changes. --- .../securesms/WebRtcCallActivity.java | 5 + .../webrtc/WebRtcCallViewModel.java | 30 +++-- .../securesms/util/livedata/LiveDataUtil.java | 19 +++ .../util/livedata/LiveDataUtilTest_skip.java | 120 ++++++++++++++++++ .../securesms/util/livedata/TestObserver.java | 22 ++++ 5 files changed, 185 insertions(+), 11 deletions(-) create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtilTest_skip.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/util/livedata/TestObserver.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 66f99b84ac..5f6a0901f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -241,6 +241,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants); viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate); viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent); + viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall()); callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> { CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); @@ -505,6 +506,10 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe } } + private void updateGroupMembersForGroupCall() { + startService(new Intent(this, WebRtcCallService.class).setAction(WebRtcCallService.ACTION_GROUP_REQUEST_UPDATE_MEMBERS)); + } + @Override public void onSendAnywayAfterSafetyNumberChange(@NonNull List changedRecipients) { CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java index 7b0b99c9e3..1969565ab4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallViewModel.java @@ -17,6 +17,8 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipantId; import org.thoughtcrime.securesms.events.WebRtcViewModel; +import org.thoughtcrime.securesms.groups.LiveGroup; +import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -30,17 +32,19 @@ import java.util.List; public class WebRtcCallViewModel extends ViewModel { - private final MutableLiveData microphoneEnabled = new MutableLiveData<>(true); - private final MutableLiveData isInPipMode = new MutableLiveData<>(false); - private final MutableLiveData webRtcControls = new MutableLiveData<>(WebRtcControls.NONE); - private final LiveData realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls); - private final SingleLiveEvent events = new SingleLiveEvent(); - private final MutableLiveData elapsed = new MutableLiveData<>(-1L); - private final MutableLiveData liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live()); - private final MutableLiveData participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE); - private final SingleLiveEvent callParticipantListUpdate = new SingleLiveEvent<>(); - private final MutableLiveData> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList()); - private final LiveData safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new); + private final MutableLiveData microphoneEnabled = new MutableLiveData<>(true); + private final MutableLiveData isInPipMode = new MutableLiveData<>(false); + private final MutableLiveData webRtcControls = new MutableLiveData<>(WebRtcControls.NONE); + private final LiveData realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls); + private final SingleLiveEvent events = new SingleLiveEvent(); + private final MutableLiveData elapsed = new MutableLiveData<>(-1L); + private final MutableLiveData liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live()); + private final MutableLiveData participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE); + private final SingleLiveEvent callParticipantListUpdate = new SingleLiveEvent<>(); + private final MutableLiveData> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList()); + private final LiveData safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new); + private final LiveData groupRecipient = LiveDataUtil.filter(Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData), Recipient::isActiveGroup); + private final LiveData> groupMembers = LiveDataUtil.skip(Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())), 1); private boolean canDisplayTooltipIfNeeded = true; private boolean hasEnabledLocalVideo = false; @@ -90,6 +94,10 @@ public class WebRtcCallViewModel extends ViewModel { return safetyNumberChangeEvent; } + public LiveData> getGroupMembers() { + return groupMembers; + } + public boolean canEnterPipMode() { return canEnterPipMode; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java index 41419d4a8b..4c817751d5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtil.java @@ -129,6 +129,25 @@ public final class LiveDataUtil { return mediatorLiveData; } + /** + * Skip the first {@param skip} emissions before emitting everything else. + */ + public static @NonNull LiveData skip(@NonNull LiveData source, int skip) { + return new MediatorLiveData() { + int skipsRemaining = skip; + + { + addSource(source, value -> { + if (skipsRemaining <= 0) { + setValue(value); + } else { + skipsRemaining--; + } + }); + } + }; + } + /** * After {@param delay} ms after observation, emits a single Object, {@param value}. */ diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtilTest_skip.java b/app/src/test/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtilTest_skip.java new file mode 100644 index 0000000000..25574f5429 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/livedata/LiveDataUtilTest_skip.java @@ -0,0 +1,120 @@ +package org.thoughtcrime.securesms.util.livedata; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import static org.junit.Assert.assertEquals; +import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.assertNoValue; +import static org.thoughtcrime.securesms.util.livedata.LiveDataTestUtil.observeAndGetOneValue; + +public final class LiveDataUtilTest_skip { + + @Rule + public TestRule rule = new LiveDataRule(); + + @Test + public void skip_no_value() { + MutableLiveData liveData = new MutableLiveData<>(); + + LiveData skipped = LiveDataUtil.skip(liveData, 0); + + assertNoValue(skipped); + } + + @Test + public void skip_same_value_with_zero_skip() { + MutableLiveData liveData = new MutableLiveData<>(); + + LiveData skipped = LiveDataUtil.skip(liveData, 0); + liveData.setValue("A"); + + assertEquals("A", observeAndGetOneValue(skipped)); + } + + @Test + public void skip_second_value_with_skip_one() { + MutableLiveData liveData = new MutableLiveData<>(); + TestObserver testObserver = new TestObserver<>(); + + LiveData skipped = LiveDataUtil.skip(liveData, 1); + + skipped.observeForever(testObserver); + liveData.setValue("A"); + liveData.setValue("B"); + skipped.removeObserver(testObserver); + + Assertions.assertThat(testObserver.getValues()) + .containsExactly("B"); + } + + @Test + public void skip_no_value_with_skip() { + MutableLiveData liveData = new MutableLiveData<>(); + + LiveData skipped = LiveDataUtil.skip(liveData, 1); + liveData.setValue("A"); + + assertNoValue(skipped); + } + + @Test + public void skip_third_and_fourth_value_with_skip_two() { + MutableLiveData liveData = new MutableLiveData<>(); + TestObserver testObserver = new TestObserver<>(); + + LiveData skipped = LiveDataUtil.skip(liveData, 2); + + skipped.observeForever(testObserver); + liveData.setValue("A"); + liveData.setValue("B"); + liveData.setValue("C"); + liveData.setValue("D"); + skipped.removeObserver(testObserver); + + Assertions.assertThat(testObserver.getValues()) + .containsExactly("C", "D"); + } + + @Test + public void skip_set_one_before_then_skip() { + MutableLiveData liveData = new MutableLiveData<>(); + TestObserver testObserver = new TestObserver<>(); + + liveData.setValue("A"); + + LiveData skipped = LiveDataUtil.skip(liveData, 2); + + skipped.observeForever(testObserver); + liveData.setValue("B"); + liveData.setValue("C"); + liveData.setValue("D"); + skipped.removeObserver(testObserver); + + Assertions.assertThat(testObserver.getValues()) + .containsExactly("C", "D"); + } + + @Test + public void skip_set_two_before_then_skip() { + MutableLiveData liveData = new MutableLiveData<>(); + TestObserver testObserver = new TestObserver<>(); + + liveData.setValue("A"); + liveData.setValue("B"); + + LiveData skipped = LiveDataUtil.skip(liveData, 2); + + skipped.observeForever(testObserver); + liveData.setValue("C"); + liveData.setValue("D"); + skipped.removeObserver(testObserver); + + Assertions.assertThat(testObserver.getValues()) + .containsExactly("D"); + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/livedata/TestObserver.java b/app/src/test/java/org/thoughtcrime/securesms/util/livedata/TestObserver.java new file mode 100644 index 0000000000..43b06265d3 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/livedata/TestObserver.java @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.util.livedata; + +import androidx.lifecycle.Observer; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Collection; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class TestObserver implements Observer { + + private final Collection values = new ConcurrentLinkedQueue<>(); + + @Override + public void onChanged(T t) { + values.add(t); + } + + public @NonNull Collection getValues() { + return values; + } +}