diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 1487275724..176b6472dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -132,7 +132,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi NotificationChannels.create(this); RefreshPreKeysJob.scheduleIfNecessary(); StorageSyncHelper.scheduleRoutineSync(); - RetrieveProfileJob.enqueueRoutineFetchIfNeccessary(this); + RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this); RegistrationUtil.maybeMarkRegistrationComplete(this); ProcessLifecycleOwner.get().getLifecycle().addObserver(this); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 103ce18461..caf0483b76 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -286,8 +286,9 @@ public final class GroupDatabase extends Database { List recipients = new ArrayList<>(currentMembers.size()); for (RecipientId member : currentMembers) { - if (memberSet.includeSelf || !Recipient.resolved(member).isLocalNumber()) { - recipients.add(Recipient.resolved(member)); + Recipient resolved = Recipient.resolved(member); + if (memberSet.includeSelf || !resolved.isLocalNumber()) { + recipients.add(resolved); } } @@ -817,9 +818,7 @@ public final class GroupDatabase extends Database { } public List getMemberRecipients(@NonNull MemberSet memberSet) { - return Stream.of(getMemberRecipientIds(memberSet)) - .map(Recipient::resolved) - .toList(); + return Recipient.resolvedList(getMemberRecipientIds(memberSet)); } public List getMemberRecipientIds(@NonNull MemberSet memberSet) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index d1cf7cca37..eaf0634b07 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -1391,13 +1391,13 @@ public class RecipientDatabase extends Database { * If from authoritative source, this will overwrite local, otherwise it will only write to the * database if missing. */ - public Collection persistProfileKeySet(@NonNull ProfileKeySet profileKeySet) { + public Set persistProfileKeySet(@NonNull ProfileKeySet profileKeySet) { Map profileKeys = profileKeySet.getProfileKeys(); Map authoritativeProfileKeys = profileKeySet.getAuthoritativeProfileKeys(); int totalKeys = profileKeys.size() + authoritativeProfileKeys.size(); if (totalKeys == 0) { - return Collections.emptyList(); + return Collections.emptySet(); } Log.i(TAG, String.format(Locale.US, "Persisting %d Profile keys, %d of which are authoritative", totalKeys, authoritativeProfileKeys.size())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 9c8ba2ed1c..9fef0482fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -65,15 +65,14 @@ final class GroupManagerV2 { private static final String TAG = Log.tag(GroupManagerV2.class); - private final Context context; - private final GroupDatabase groupDatabase; - private final GroupsV2Api groupsV2Api; - private final GroupsV2Operations groupsV2Operations; - private final GroupsV2Authorization authorization; - private final GroupsV2StateProcessor groupsV2StateProcessor; - private final UUID selfUuid; - private final GroupCandidateHelper groupCandidateHelper; - private final GroupsV2CapabilityChecker capabilityChecker; + private final Context context; + private final GroupDatabase groupDatabase; + private final GroupsV2Api groupsV2Api; + private final GroupsV2Operations groupsV2Operations; + private final GroupsV2Authorization authorization; + private final GroupsV2StateProcessor groupsV2StateProcessor; + private final UUID selfUuid; + private final GroupCandidateHelper groupCandidateHelper; GroupManagerV2(@NonNull Context context) { this.context = context; @@ -84,7 +83,6 @@ final class GroupManagerV2 { this.groupsV2StateProcessor = ApplicationDependencies.getGroupsV2StateProcessor(); this.selfUuid = Recipient.self().getUuid().get(); this.groupCandidateHelper = new GroupCandidateHelper(context); - this.capabilityChecker = new GroupsV2CapabilityChecker(); } @WorkerThread @@ -116,7 +114,7 @@ final class GroupManagerV2 { @Nullable byte[] avatar) throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception { - if (!capabilityChecker.allAndSelfSupportGroupsV2AndUuid(members)) { + if (!GroupsV2CapabilityChecker.allAndSelfSupportGroupsV2AndUuid(members)) { throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities"); } @@ -196,7 +194,7 @@ final class GroupManagerV2 { @NonNull GroupManager.GroupActionResult addMembers(@NonNull Collection newMembers) throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, MembershipNotSuitableForV2Exception { - if (!capabilityChecker.allSupportGroupsV2AndUuid(newMembers)) { + if (!GroupsV2CapabilityChecker.allSupportGroupsV2AndUuid(newMembers)) { throw new MembershipNotSuitableForV2Exception("At least one potential new member does not support GV2 or UUID capabilities"); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2CapabilityChecker.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2CapabilityChecker.java index 1d60baae4a..c3d14551b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2CapabilityChecker.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupsV2CapabilityChecker.java @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.groups; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; +import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; @@ -24,17 +25,17 @@ public final class GroupsV2CapabilityChecker { private static final String TAG = Log.tag(GroupsV2CapabilityChecker.class); - public GroupsV2CapabilityChecker() {} + private GroupsV2CapabilityChecker() {} /** * @param resolved A collection of resolved recipients. */ @WorkerThread - public void refreshCapabilitiesIfNecessary(@NonNull Collection resolved) throws IOException { - List needsRefresh = Stream.of(resolved) - .filter(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED) - .map(Recipient::getId) - .toList(); + public static void refreshCapabilitiesIfNecessary(@NonNull Collection resolved) throws IOException { + Set needsRefresh = Stream.of(resolved) + .filter(r -> r.getGroupsV2Capability() != Recipient.Capability.SUPPORTED) + .map(Recipient::getId) + .collect(Collectors.toSet()); if (needsRefresh.size() > 0) { Log.d(TAG, "[refreshCapabilitiesIfNecessary] Need to refresh " + needsRefresh.size() + " recipients."); @@ -51,7 +52,7 @@ public final class GroupsV2CapabilityChecker { } @WorkerThread - boolean allAndSelfSupportGroupsV2AndUuid(@NonNull Collection recipientIds) + static boolean allAndSelfSupportGroupsV2AndUuid(@NonNull Collection recipientIds) throws IOException { HashSet recipientIdsSet = new HashSet<>(recipientIds); @@ -62,7 +63,7 @@ public final class GroupsV2CapabilityChecker { } @WorkerThread - boolean allSupportGroupsV2AndUuid(@NonNull Collection recipientIds) + static boolean allSupportGroupsV2AndUuid(@NonNull Collection recipientIds) throws IOException { Set recipientIdsSet = new HashSet<>(recipientIds); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java index 96ea29bcd2..5111a24a79 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.java @@ -152,7 +152,7 @@ public class CreateGroupActivity extends ContactSelectionActivity { if (FeatureFlags.groupsV2create()) { try { - new GroupsV2CapabilityChecker().refreshCapabilitiesIfNecessary(resolved); + GroupsV2CapabilityChecker.refreshCapabilitiesIfNecessary(resolved); } catch (IOException e) { Log.w(TAG, "Failed to refresh all recipient capabilities.", e); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index 3752ef43de..eb710d4126 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -25,7 +25,9 @@ import org.thoughtcrime.securesms.groups.GroupNotAMemberException; import org.thoughtcrime.securesms.groups.GroupProtoUtil; import org.thoughtcrime.securesms.groups.GroupsV2Authorization; import org.thoughtcrime.securesms.groups.v2.ProfileKeySet; +import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; +import org.thoughtcrime.securesms.jobmanager.JobTracker; import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob; import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; @@ -51,6 +53,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.UUID; /** @@ -297,11 +300,14 @@ public final class GroupsV2StateProcessor { } } - Collection updated = recipientDatabase.persistProfileKeySet(profileKeys); + Set updated = recipientDatabase.persistProfileKeySet(profileKeys); if (!updated.isEmpty()) { - Log.i(TAG, String.format(Locale.US, "Learned %d new profile keys, scheduling profile retrievals", updated.size())); - RetrieveProfileJob.enqueue(updated); + Log.i(TAG, String.format(Locale.US, "Learned %d new profile keys, fetching profiles", updated.size())); + + for (Job job : RetrieveProfileJob.forRecipients(updated)) { + jobManager.runSynchronously(job, 5000); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index 3568e4d0a5..52a932648d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -65,7 +65,7 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; -public class PushGroupSendJob extends PushSendJob { +public final class PushGroupSendJob extends PushSendJob { public static final String KEY = "PushGroupSendJob"; @@ -74,8 +74,8 @@ public class PushGroupSendJob extends PushSendJob { private static final String KEY_MESSAGE_ID = "message_id"; private static final String KEY_FILTER_RECIPIENT = "filter_recipient"; - private long messageId; - private RecipientId filterRecipient; + private final long messageId; + private final RecipientId filterRecipient; public PushGroupSendJob(long messageId, @NonNull RecipientId destination, @Nullable RecipientId filterRecipient, boolean hasMedia) { this(new Job.Parameters.Builder() @@ -237,7 +237,10 @@ public class PushGroupSendJob extends PushSendJob { database.markAsSentFailed(messageId); notifyMediaMessageDeliveryFailed(context, messageId); - List mismatchRecipientIds = Stream.of(identityMismatches).map(mismatch -> mismatch.getRecipientId(context)).toList(); + Set mismatchRecipientIds = Stream.of(identityMismatches) + .map(mismatch -> mismatch.getRecipientId(context)) + .collect(Collectors.toSet()); + RetrieveProfileJob.enqueue(mismatchRecipientIds); } } catch (UntrustedIdentityException | UndeliverableMessageException e) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java index 5bf55715fc..87059569b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RetrieveProfileJob.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.jobs; - import android.app.Application; import android.content.Context; import android.text.TextUtils; @@ -9,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; +import com.annimon.stream.Collectors; import com.annimon.stream.Stream; import org.signal.zkgroup.profiles.ProfileKey; @@ -50,10 +50,9 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture; import java.io.IOException; -import java.util.Collection; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -73,22 +72,20 @@ public class RetrieveProfileJob extends BaseJob { private static final String KEY_RECIPIENTS = "recipients"; - private final List recipientIds; + private final Set recipientIds; /** - * Identical to {@link #enqueue(Collection)})}, but run on a background thread for convenience. + * Identical to {@link #enqueue(Set)})}, but run on a background thread for convenience. */ public static void enqueueAsync(@NonNull RecipientId recipientId) { - SignalExecutors.BOUNDED.execute(() -> { - ApplicationDependencies.getJobManager().add(forRecipient(recipientId)); - }); + SignalExecutors.BOUNDED.execute(() -> ApplicationDependencies.getJobManager().add(forRecipient(recipientId))); } /** * Submits the necessary job to refresh the profile of the requested recipient. Works for any * RecipientId, including individuals, groups, or yourself. * - * Identical to {@link #enqueue(Collection)})} + * Identical to {@link #enqueue(Set)})} */ @WorkerThread public static void enqueue(@NonNull RecipientId recipientId) { @@ -100,7 +97,7 @@ public class RetrieveProfileJob extends BaseJob { * RecipientIds, including individuals, groups, or yourself. */ @WorkerThread - public static void enqueue(@NonNull Collection recipientIds) { + public static void enqueue(@NonNull Set recipientIds) { JobManager jobManager = ApplicationDependencies.getJobManager(); for (Job job : forRecipients(recipientIds)) { @@ -121,26 +118,28 @@ public class RetrieveProfileJob extends BaseJob { Context context = ApplicationDependencies.getApplication(); List recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); - return new RetrieveProfileJob(Stream.of(recipients).map(Recipient::getId).toList()); + return new RetrieveProfileJob(Stream.of(recipients).map(Recipient::getId).collect(Collectors.toSet())); } else { - return new RetrieveProfileJob(Collections.singletonList(recipientId)); + return new RetrieveProfileJob(Collections.singleton(recipientId)); } } /** * Works for any RecipientId, whether it's an individual, group, or yourself. + * + * @return A list of length 2 or less. Two iff you are in the recipients. */ @WorkerThread - public static @NonNull List forRecipients(@NonNull Collection recipientIds) { - Context context = ApplicationDependencies.getApplication(); - List combined = new LinkedList<>(); - List jobs = new LinkedList<>(); + public static @NonNull List forRecipients(@NonNull Set recipientIds) { + Context context = ApplicationDependencies.getApplication(); + Set combined = new HashSet<>(recipientIds.size()); + boolean includeSelf = false; for (RecipientId recipientId : recipientIds) { Recipient recipient = Recipient.resolved(recipientId); if (recipient.isLocalNumber()) { - jobs.add(new RefreshOwnProfileJob()); + includeSelf = true; } else if (recipient.isGroup()) { List recipients = DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF); combined.addAll(Stream.of(recipients).map(Recipient::getId).toList()); @@ -149,7 +148,15 @@ public class RetrieveProfileJob extends BaseJob { } } - jobs.add(new RetrieveProfileJob(combined)); + List jobs = new ArrayList<>(2); + + if (includeSelf) { + jobs.add(new RefreshOwnProfileJob()); + } + + if (combined.size() > 0) { + jobs.add(new RetrieveProfileJob(combined)); + } return jobs; } @@ -158,7 +165,7 @@ public class RetrieveProfileJob extends BaseJob { * Will fetch some profiles to ensure we're decently up-to-date if we haven't done so within a * certain time period. */ - public static void enqueueRoutineFetchIfNeccessary(Application application) { + public static void enqueueRoutineFetchIfNecessary(Application application) { long timeSinceRefresh = System.currentTimeMillis() - SignalStore.misc().getLastProfileRefreshTime(); if (timeSinceRefresh < TimeUnit.HOURS.toMillis(12)) { Log.i(TAG, "Too soon to refresh. Did the last refresh " + timeSinceRefresh + " ms ago."); @@ -175,7 +182,7 @@ public class RetrieveProfileJob extends BaseJob { if (ids.size() > 0) { Log.i(TAG, "Optimistically refreshing " + ids.size() + " eligible recipient(s)."); - enqueue(ids); + enqueue(new HashSet<>(ids)); } else { Log.i(TAG, "No recipients to refresh."); } @@ -184,7 +191,7 @@ public class RetrieveProfileJob extends BaseJob { }); } - private RetrieveProfileJob(@NonNull List recipientIds) { + private RetrieveProfileJob(@NonNull Set recipientIds) { this(new Job.Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(3) @@ -192,7 +199,7 @@ public class RetrieveProfileJob extends BaseJob { recipientIds); } - private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull List recipientIds) { + private RetrieveProfileJob(@NonNull Job.Parameters parameters, @NonNull Set recipientIds) { super(parameters); this.recipientIds = recipientIds; } @@ -279,7 +286,7 @@ public class RetrieveProfileJob extends BaseJob { @Override public void onFailure() {} - private void process(Recipient recipient, ProfileAndCredential profileAndCredential) throws IOException { + private void process(Recipient recipient, ProfileAndCredential profileAndCredential) { SignalServiceProfile profile = profileAndCredential.getProfile(); ProfileKey recipientProfileKey = ProfileKeyUtil.profileKeyOrNull(recipient.getProfileKey()); @@ -410,7 +417,7 @@ public class RetrieveProfileJob extends BaseJob { } } - private void setProfileAvatar(Recipient recipient, String profileAvatar) { + private static void setProfileAvatar(Recipient recipient, String profileAvatar) { if (recipient.getProfileKey() == null) return; if (!Util.equals(profileAvatar, recipient.getProfileAvatar())) { @@ -434,8 +441,8 @@ public class RetrieveProfileJob extends BaseJob { @Override public @NonNull RetrieveProfileJob create(@NonNull Parameters parameters, @NonNull Data data) { - String[] ids = data.getStringArray(KEY_RECIPIENTS); - List recipientIds = Stream.of(ids).map(RecipientId::from).toList(); + String[] ids = data.getStringArray(KEY_RECIPIENTS); + Set recipientIds = Stream.of(ids).map(RecipientId::from).collect(Collectors.toSet()); return new RetrieveProfileJob(parameters, recipientIds); }