From a510bc74e633d13393499158a95a237309647c50 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Mon, 11 May 2020 12:33:45 -0300 Subject: [PATCH] Recipient Id cache. --- .../securesms/recipients/LiveRecipient.java | 16 +- .../securesms/recipients/RecipientId.java | 22 ++ .../recipients/RecipientIdCache.java | 73 +++++ .../securesms/util/GroupUtil.java | 36 +-- .../recipients/RecipientIdCacheTest.java | 273 ++++++++++++++++++ .../securesms/testutil/LogRecorder.java | 98 +++++++ 6 files changed, 493 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientIdCache.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/recipients/RecipientIdCacheTest.java create mode 100644 app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java 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 eb91a97017..a216167bfc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/LiveRecipient.java @@ -128,11 +128,11 @@ public final class LiveRecipient { Log.w(TAG, "[Resolve][MAIN] " + getId(), new Throwable()); } - Recipient updated = fetchRecipientFromDisk(getId()); + Recipient updated = fetchAndCacheRecipientFromDisk(getId()); List participants = Stream.of(updated.getParticipants()) .filter(Recipient::isResolving) .map(Recipient::getId) - .map(this::fetchRecipientFromDisk) + .map(this::fetchAndCacheRecipientFromDisk) .toList(); for (Recipient participant : participants) { @@ -155,10 +155,10 @@ public final class LiveRecipient { Log.w(TAG, "[Refresh][MAIN] " + getId(), new Throwable()); } - Recipient recipient = fetchRecipientFromDisk(getId()); + Recipient recipient = fetchAndCacheRecipientFromDisk(getId()); List participants = Stream.of(recipient.getParticipants()) .map(Recipient::getId) - .map(this::fetchRecipientFromDisk) + .map(this::fetchAndCacheRecipientFromDisk) .toList(); for (Recipient participant : participants) { @@ -172,12 +172,14 @@ public final class LiveRecipient { return liveData; } - private @NonNull Recipient fetchRecipientFromDisk(RecipientId id) { + private @NonNull Recipient fetchAndCacheRecipientFromDisk(@NonNull RecipientId id) { RecipientSettings settings = recipientDatabase.getRecipientSettings(id); RecipientDetails details = settings.getGroupId() != null ? getGroupRecipientDetails(settings) : getIndividualRecipientDetails(settings); - return new Recipient(id, details); + Recipient recipient = new Recipient(id, details); + RecipientIdCache.INSTANCE.put(recipient); + return recipient; } private @NonNull RecipientDetails getIndividualRecipientDetails(RecipientSettings settings) { @@ -194,7 +196,7 @@ public final class LiveRecipient { if (groupRecord.isPresent()) { String title = groupRecord.get().getTitle(); - List members = Stream.of(groupRecord.get().getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchRecipientFromDisk).toList(); + List members = Stream.of(groupRecord.get().getMembers()).filterNot(RecipientId::isUnknown).map(this::fetchAndCacheRecipientFromDisk).toList(); Optional avatarId = Optional.absent(); if (settings.getGroupId() != null && settings.getGroupId().isPush() && title == null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java index aa96ade38e..93b05287a4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientId.java @@ -1,17 +1,24 @@ package org.thoughtcrime.securesms.recipients; +import android.annotation.SuppressLint; import android.os.Parcel; import android.os.Parcelable; +import androidx.annotation.AnyThread; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.annimon.stream.Stream; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.util.DelimiterUtil; import org.thoughtcrime.securesms.util.Util; import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.UUID; import java.util.regex.Pattern; public class RecipientId implements Parcelable, Comparable { @@ -39,6 +46,21 @@ public class RecipientId implements Parcelable, Comparable { } } + /** + * Always supply both {@param uuid} and {@param e164} if you have both. + */ + @AnyThread + @SuppressLint("WrongThread") + public static @NonNull RecipientId from(@Nullable UUID uuid, @Nullable String e164) { + RecipientId recipientId = RecipientIdCache.INSTANCE.get(uuid, e164); + + if (recipientId == null) { + recipientId = Recipient.externalPush(ApplicationDependencies.getApplication(), uuid, e164).getId(); + } + + return recipientId; + } + private RecipientId(long id) { this.id = id; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientIdCache.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientIdCache.java new file mode 100644 index 0000000000..7c60c1b646 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientIdCache.java @@ -0,0 +1,73 @@ +package org.thoughtcrime.securesms.recipients; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.logging.Log; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Thread safe cache that allows faster looking up of {@link RecipientId}s without hitting the database. + */ +final class RecipientIdCache { + + private static final int INSTANCE_CACHE_LIMIT = 1000; + + static final RecipientIdCache INSTANCE = new RecipientIdCache(INSTANCE_CACHE_LIMIT); + + private static final String TAG = Log.tag(RecipientIdCache.class); + + private final Map ids; + + RecipientIdCache(int limit) { + ids = new LinkedHashMap(128, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > limit; + } + }; + } + + synchronized void put(@NonNull Recipient recipient) { + RecipientId recipientId = recipient.getId(); + Optional e164 = recipient.getE164(); + Optional uuid = recipient.getUuid(); + + if (e164.isPresent()) { + ids.put(e164.get(), recipientId); + } + + if (uuid.isPresent()) { + ids.put(uuid.get(), recipientId); + } + } + + synchronized @Nullable RecipientId get(@Nullable UUID uuid, @Nullable String e164) { + if (uuid != null && e164 != null) { + RecipientId recipientIdByUuid = ids.get(uuid); + if (recipientIdByUuid == null) return null; + + RecipientId recipientIdByE164 = ids.get(e164); + if (recipientIdByE164 == null) return null; + + if (recipientIdByUuid.equals(recipientIdByE164)) { + return recipientIdByUuid; + } else { + ids.remove(uuid); + ids.remove(e164); + Log.w(TAG, "Seen invalid RecipientIdCacheState"); + return null; + } + } else if (uuid != null) { + return ids.get(uuid); + } else if (e164 != null) { + return ids.get(e164); + } + + return null; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java index 3bf1821eb1..ac12908fad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java @@ -17,12 +17,12 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -129,11 +129,11 @@ public final class GroupUtil { public static class GroupDescription { - @NonNull private final Context context; - @Nullable private final GroupContext groupContext; - @Nullable private final List members; + @NonNull private final Context context; + @Nullable private final GroupContext groupContext; + @Nullable private final List members; - public GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { + GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { this.context = context.getApplicationContext(); this.groupContext = groupContext; @@ -143,9 +143,9 @@ public final class GroupUtil { this.members = new LinkedList<>(); for (GroupContext.Member member : groupContext.getMembersList()) { - Recipient recipient = Recipient.externalPush(context, new SignalServiceAddress(UuidUtil.parseOrNull(member.getUuid()), member.getE164())); - if (!recipient.isLocalNumber()) { - this.members.add(recipient); + RecipientId recipientId = RecipientId.from(UuidUtil.parseOrNull(member.getUuid()), member.getE164()); + if (!recipientId.equals(Recipient.self().getId())) { + this.members.add(recipientId); } } } @@ -178,31 +178,31 @@ public final class GroupUtil { public void addObserver(RecipientForeverObserver listener) { if (this.members != null) { - for (Recipient member : this.members) { - member.live().observeForever(listener); + for (RecipientId member : this.members) { + Recipient.live(member).observeForever(listener); } } } public void removeObserver(RecipientForeverObserver listener) { if (this.members != null) { - for (Recipient member : this.members) { - member.live().removeForeverObserver(listener); + for (RecipientId member : this.members) { + Recipient.live(member).removeForeverObserver(listener); } } } - private String toString(List recipients) { - String result = ""; + private String toString(List recipients) { + StringBuilder result = new StringBuilder(); - for (int i=0;i verbose = new ArrayList<>(); + private final List debug = new ArrayList<>(); + private final List information = new ArrayList<>(); + private final List warnings = new ArrayList<>(); + private final List errors = new ArrayList<>(); + private final List wtf = new ArrayList<>(); + + @Override + public void v(String tag, String message, Throwable t) { + verbose.add(new Entry(tag, message, t)); + } + + @Override + public void d(String tag, String message, Throwable t) { + debug.add(new Entry(tag, message, t)); + } + + @Override + public void i(String tag, String message, Throwable t) { + information.add(new Entry(tag, message, t)); + } + + @Override + public void w(String tag, String message, Throwable t) { + warnings.add(new Entry(tag, message, t)); + } + + @Override + public void e(String tag, String message, Throwable t) { + errors.add(new Entry(tag, message, t)); + } + + @Override + public void wtf(String tag, String message, Throwable t) { + wtf.add(new Entry(tag, message, t)); + } + + @Override + public void blockUntilAllWritesFinished() { + } + + public List getVerbose() { + return verbose; + } + + public List getDebug() { + return debug; + } + + public List getInformation() { + return information; + } + + public List getWarnings() { + return warnings; + } + + public List getErrors() { + return errors; + } + + public List getWtf() { + return wtf; + } + + public static final class Entry { + private final String tag; + private final String message; + private final Throwable throwable; + + private Entry(String tag, String message, Throwable throwable) { + this.tag = tag; + this.message = message; + this.throwable = throwable; + } + + public String getTag() { + return tag; + } + + public String getMessage() { + return message; + } + + public Throwable getThrowable() { + return throwable; + } + } +}