diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java index e189d5e760..09dffb3cac 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.util.Util; +import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.whispersystems.libsignal.util.guava.Optional; import java.util.List; @@ -32,6 +33,7 @@ public final class LiveRecipient { private final Context context; private final MutableLiveData liveData; + private final LiveData observableLiveData; private final Set observers; private final Observer foreverObserver; private final AtomicReference recipient; @@ -50,6 +52,7 @@ public final class LiveRecipient { o.onRecipientChanged(recipient); } }; + this.observableLiveData = LiveDataUtil.distinctUntilChanged(liveData, Recipient::hasSameContent); } public @NonNull RecipientId getId() { @@ -70,14 +73,14 @@ public final class LiveRecipient { * use {@link #removeObservers(LifecycleOwner)}. */ public void observe(@NonNull LifecycleOwner owner, @NonNull Observer observer) { - Util.postToMain(() -> liveData.observe(owner, observer)); + Util.postToMain(() -> observableLiveData.observe(owner, observer)); } /** * Removes all observers of this data registered for the given LifecycleOwner. */ public void removeObservers(@NonNull LifecycleOwner owner) { - Util.runOnMain(() -> liveData.removeObservers(owner)); + Util.runOnMain(() -> observableLiveData.removeObservers(owner)); } /** @@ -88,7 +91,7 @@ public final class LiveRecipient { public void observeForever(@NonNull RecipientForeverObserver observer) { Util.postToMain(() -> { if (observers.isEmpty()) { - liveData.observeForever(foreverObserver); + observableLiveData.observeForever(foreverObserver); } observers.add(observer); }); @@ -102,7 +105,7 @@ public final class LiveRecipient { observers.remove(observer); if (observers.isEmpty()) { - liveData.removeObserver(foreverObserver); + observableLiveData.removeObserver(foreverObserver); } }); } @@ -172,7 +175,7 @@ public final class LiveRecipient { } public @NonNull LiveData getLiveData() { - return liveData; + return observableLiveData; } private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 2606296d44..bef11d5a8e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -47,6 +47,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -873,6 +874,12 @@ public class Recipient { return id.equals(recipient.id); } + @Override + public int hashCode() { + return Objects.hash(id); + } + + public enum Capability { UNKNOWN(0), SUPPORTED(1), @@ -902,11 +909,64 @@ public class Recipient { } } - @Override - public int hashCode() { - return Objects.hash(id); + public boolean hasSameContent(@NonNull Recipient other) { + return Objects.equals(id, other.id) && + resolving == other.resolving && + isSelf == other.isSelf && + blocked == other.blocked && + muteUntil == other.muteUntil && + expireMessages == other.expireMessages && + hasProfileImage == other.hasProfileImage && + profileSharing == other.profileSharing && + lastProfileFetch == other.lastProfileFetch && + forceSmsSelection == other.forceSmsSelection && + Objects.equals(id, other.id) && + Objects.equals(uuid, other.uuid) && + Objects.equals(username, other.username) && + Objects.equals(e164, other.e164) && + Objects.equals(email, other.email) && + Objects.equals(groupId, other.groupId) && + allContentsAreTheSame(participants, other.participants) && + Objects.equals(groupAvatarId, other.groupAvatarId) && + messageVibrate == other.messageVibrate && + callVibrate == other.callVibrate && + Objects.equals(messageRingtone, other.messageRingtone) && + Objects.equals(callRingtone, other.callRingtone) && + color == other.color && + Objects.equals(defaultSubscriptionId, other.defaultSubscriptionId) && + registered == other.registered && + Arrays.equals(profileKey, other.profileKey) && + Objects.equals(profileKeyCredential, other.profileKeyCredential) && + Objects.equals(name, other.name) && + Objects.equals(systemContactPhoto, other.systemContactPhoto) && + Objects.equals(customLabel, other.customLabel) && + Objects.equals(contactUri, other.contactUri) && + Objects.equals(profileName, other.profileName) && + Objects.equals(profileAvatar, other.profileAvatar) && + Objects.equals(notificationChannel, other.notificationChannel) && + unidentifiedAccessMode == other.unidentifiedAccessMode && + groupsV2Capability == other.groupsV2Capability && + groupsV1MigrationCapability == other.groupsV1MigrationCapability && + insightsBannerTier == other.insightsBannerTier && + Arrays.equals(storageId, other.storageId) && + mentionSetting == other.mentionSetting; } + private static boolean allContentsAreTheSame(@NonNull List a, @NonNull List b) { + if (a.size() != b.size()) { + return false; + } + + for (int i = 0, len = a.size(); i < len; i++) { + if (!a.get(i).hasSameContent(b.get(i))) { + return false; + } + } + + return true; + } + + public static class FallbackPhotoProvider { public @NonNull FallbackContactPhoto getPhotoForLocalNumber() { return new ResourceContactPhoto(R.drawable.ic_note_34, R.drawable.ic_note_24); 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 cfce98db10..f7ac85ab50 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 @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; import com.annimon.stream.function.Predicate; @@ -169,10 +170,42 @@ public final class LiveDataUtil { }; } + public static LiveData distinctUntilChanged(@NonNull LiveData source, @NonNull EqualityChecker checker) { + final MediatorLiveData outputLiveData = new MediatorLiveData<>(); + outputLiveData.addSource(source, new Observer() { + + boolean firstChange = true; + + @Override + public void onChanged(T nextValue) { + T currentValue = outputLiveData.getValue(); + + if (currentValue == null && nextValue == null) { + return; + } + + if (firstChange || + currentValue == null || + nextValue == null || + !checker.contentsMatch(currentValue, nextValue)) + { + firstChange = false; + outputLiveData.setValue(nextValue); + } + } + }); + + return outputLiveData; + } + public interface Combine { @NonNull R apply(@NonNull A a, @NonNull B b); } + public interface EqualityChecker { + boolean contentsMatch(@NonNull T current, @NonNull T next); + } + private static final class CombineLiveData extends MediatorLiveData { private A a; private B b;