From befb4939d5b7ef665657d46d04ee344f3567c923 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Tue, 26 May 2020 16:02:34 -0300 Subject: [PATCH] Restore groups from storage service. --- .../securesms/database/RecipientDatabase.java | 4 +- .../v2/processing/GroupsV2StateProcessor.java | 16 +- .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/jobs/WakeGroupV2Job.java | 139 ++++++++++++++++++ 4 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/WakeGroupV2Job.java 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 35033a47a6..010a286738 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -25,7 +25,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.v2.ProfileKeySet; -import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob; +import org.thoughtcrime.securesms.jobs.WakeGroupV2Job; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; @@ -619,7 +619,7 @@ public class RecipientDatabase extends Database { GroupId.V2 groupId = GroupId.v2(insert.getMasterKey()); Recipient recipient = Recipient.externalGroup(context, groupId); - ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId)); + ApplicationDependencies.getJobManager().add(new WakeGroupV2Job(insert.getMasterKey())); threadDatabase.setArchived(recipient.getId(), insert.isArchived()); recipient.live().refresh(); 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 9ac584a53a..7460f35c5b 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 @@ -165,7 +165,7 @@ public final class GroupsV2StateProcessor { if (inputGroupState == null) { try { - inputGroupState = queryServer(localState); + inputGroupState = queryServer(localState, revision == LATEST && localState == null); } catch (GroupNotAMemberException e) { Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message"); insertGroupLeave(); @@ -297,7 +297,7 @@ public final class GroupsV2StateProcessor { } } - private @NonNull GlobalGroupState queryServer(@Nullable DecryptedGroup localState) + private @NonNull GlobalGroupState queryServer(@Nullable DecryptedGroup localState, boolean latestOnly) throws IOException, GroupNotAMemberException { DecryptedGroup latestServerGroup; @@ -312,13 +312,13 @@ public final class GroupsV2StateProcessor { throw new IOException(e); } - int versionWeWereAdded = GroupProtoUtil.findVersionWeWereAdded(latestServerGroup, selfUuid); - int logsNeededFrom = localState != null ? Math.max(localState.getVersion(), versionWeWereAdded) : versionWeWereAdded; - - if (GroupProtoUtil.isMember(selfUuid, latestServerGroup.getMembersList())) { - history = getFullMemberHistory(selfUuid, logsNeededFrom); - } else { + if (latestOnly || !GroupProtoUtil.isMember(selfUuid, latestServerGroup.getMembersList())) { history = Collections.singletonList(new GroupLogEntry(latestServerGroup, null)); + } else { + int versionWeWereAdded = GroupProtoUtil.findVersionWeWereAdded(latestServerGroup, selfUuid); + int logsNeededFrom = localState != null ? Math.max(localState.getVersion(), versionWeWereAdded) : versionWeWereAdded; + + history = getFullMemberHistory(selfUuid, logsNeededFrom); } return new GlobalGroupState(localState, history); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index ca79e4b2ed..be20d6c9cc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -92,6 +92,7 @@ public final class JobManagerFactories { put(ResumableUploadSpecJob.KEY, new ResumableUploadSpecJob.Factory()); put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory()); put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory()); + put(WakeGroupV2Job.KEY, new WakeGroupV2Job.Factory()); put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory()); put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory()); put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/WakeGroupV2Job.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/WakeGroupV2Job.java new file mode 100644 index 0000000000..fb82d7f87b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/WakeGroupV2Job.java @@ -0,0 +1,139 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; +import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; +import org.thoughtcrime.securesms.groups.GroupChangeBusyException; +import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.groups.GroupManager; +import org.thoughtcrime.securesms.groups.GroupNotAMemberException; +import org.thoughtcrime.securesms.groups.GroupProtoUtil; +import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.mms.MmsException; +import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.Hex; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; + +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + * Use to create and show a thread for an unknown GV2 group. + */ +public final class WakeGroupV2Job extends BaseJob { + + public static final String KEY = "WakeGroupV2Job"; + + private static final String TAG = Log.tag(WakeGroupV2Job.class); + + private static final String KEY_GROUP_MASTER_KEY = "group_id"; + + private final GroupMasterKey groupMasterKey; + + public WakeGroupV2Job(@NonNull GroupMasterKey groupMasterKey) { + this(new Parameters.Builder() + .setQueue("RequestGroupV2InfoJob::" + GroupId.v2(groupMasterKey)) + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + groupMasterKey); + } + + private WakeGroupV2Job(@NonNull Parameters parameters, @NonNull GroupMasterKey groupMasterKey) { + super(parameters); + + this.groupMasterKey = groupMasterKey; + } + + @Override + public @NonNull Data serialize() { + return new Data.Builder().putString(KEY_GROUP_MASTER_KEY, Hex.toStringCondensed(groupMasterKey.serialize())) + .build(); + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + @Override + public void onRun() throws IOException, GroupNotAMemberException, GroupChangeBusyException { + if (!FeatureFlags.groupsV2()) { + Log.w(TAG, "Group wake skipped due to feature flag"); + return; + } + + Log.i(TAG, "Waking group"); + + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + GroupId.V2 groupId = GroupId.v2(groupMasterKey); + + if (!groupDatabase.getGroup(groupId).isPresent()) { + GroupManager.updateGroupFromServer(context, groupMasterKey, GroupsV2StateProcessor.LATEST, System.currentTimeMillis(), null); + Log.i(TAG, "Group created " + groupId); + } else { + Log.w(TAG, "Group already exists " + groupId); + } + + Optional group = groupDatabase.getGroup(groupId); + if (!group.isPresent()) { + Log.w(TAG, "Failed to create group from server " + groupId); + return; + } + + Log.i(TAG, "Waking group " + groupId); + try { + Recipient groupRecipient = Recipient.externalGroup(context, groupId); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + GroupDatabase.V2GroupProperties v2GroupProperties = group.get().requireV2GroupProperties(); + DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(v2GroupProperties.getGroupMasterKey(), v2GroupProperties.getDecryptedGroup(), null, null); + MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); + OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, decryptedGroupV2Context, null, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList()); + + long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); + + mmsDatabase.markAsSent(messageId, true); + } catch (MmsException e) { + Log.w(TAG, e); + } + } + + @Override + public boolean onShouldRetry(@NonNull Exception e) { + return e instanceof PushNetworkException || + e instanceof NoCredentialForRedemptionTimeException || + e instanceof GroupChangeBusyException; + } + + @Override + public void onFailure() { + } + + public static final class Factory implements Job.Factory { + + @Override + public @NonNull WakeGroupV2Job create(@NonNull Parameters parameters, @NonNull Data data) { + try { + return new WakeGroupV2Job(parameters, + new GroupMasterKey(Hex.fromStringCondensed(data.getString(KEY_GROUP_MASTER_KEY)))); + } catch (InvalidInputException | IOException e) { + throw new AssertionError(e); + } + } + } +}