diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
index 9a37b7094d..1504f33c32 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java
@@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
+import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.UuidCiphertext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
@@ -120,12 +121,13 @@ public final class GroupManager {
@WorkerThread
public static void updateGroupFromServer(@NonNull Context context,
- @NonNull GroupId.V2 groupId,
- int version)
+ @NonNull GroupMasterKey groupMasterKey,
+ int version,
+ long timestamp)
throws GroupChangeBusyException, IOException, GroupNotAMemberException
{
- try (GroupManagerV2.GroupEditor edit = new GroupManagerV2(context).edit(groupId)) {
- edit.updateLocalToServerVersion(version);
+ try (GroupManagerV2.GroupUpdater updater = new GroupManagerV2(context).updater(groupMasterKey)) {
+ updater.updateLocalToServerVersion(version, timestamp);
}
}
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 c3e7268d1e..a14c392dcd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java
@@ -89,6 +89,11 @@ final class GroupManagerV2 {
return new GroupEditor(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
}
+ @WorkerThread
+ GroupUpdater updater(@NonNull GroupMasterKey groupId) throws GroupChangeBusyException {
+ return new GroupUpdater(groupId, GroupsV2ProcessingLock.acquireGroupProcessingLock());
+ }
+
class GroupCreator implements Closeable {
private final Closeable lock;
@@ -316,14 +321,6 @@ final class GroupManagerV2 {
return commitChangeWithConflictResolution(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get()));
}
- @WorkerThread
- void updateLocalToServerVersion(int version)
- throws IOException, GroupNotAMemberException
- {
- new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
- .updateLocalGroupToRevision(version, System.currentTimeMillis());
- }
-
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
@@ -421,6 +418,30 @@ final class GroupManagerV2 {
}
}
+ class GroupUpdater implements Closeable {
+
+ private final Closeable lock;
+ private final GroupMasterKey groupMasterKey;
+
+ GroupUpdater(@NonNull GroupMasterKey groupMasterKey, @NonNull Closeable lock) {
+ this.lock = lock;
+ this.groupMasterKey = groupMasterKey;
+ }
+
+ @WorkerThread
+ void updateLocalToServerVersion(int version, long timestamp)
+ throws IOException, GroupNotAMemberException
+ {
+ new GroupsV2StateProcessor(context).forGroup(groupMasterKey)
+ .updateLocalGroupToRevision(version, timestamp);
+ }
+
+ @Override
+ public void close() throws IOException {
+ lock.close();
+ }
+ }
+
private @NonNull GroupManager.GroupActionResult sendGroupUpdate(@NonNull GroupMasterKey masterKey,
@NonNull DecryptedGroup decryptedGroup,
@Nullable DecryptedGroupChange plainGroupChange)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV2UpdateSelfProfileKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV2UpdateSelfProfileKeyJob.java
new file mode 100644
index 0000000000..12bdebe08b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupV2UpdateSelfProfileKeyJob.java
@@ -0,0 +1,92 @@
+package org.thoughtcrime.securesms.jobs;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
+import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
+import org.thoughtcrime.securesms.groups.GroupId;
+import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
+import org.thoughtcrime.securesms.groups.GroupManager;
+import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
+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.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
+import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * When your profile key changes, this job can be used to update it on a single given group.
+ *
+ * Your membership is confirmed first, so safe to run against any known {@link GroupId.V2}
+ */
+public final class GroupV2UpdateSelfProfileKeyJob extends BaseJob {
+
+ public static final String KEY = "GroupV2UpdateSelfProfileKeyJob";
+
+ private static final String QUEUE = "GroupV2UpdateSelfProfileKeyJob";
+
+ @SuppressWarnings("unused")
+ private static final String TAG = Log.tag(GroupV2UpdateSelfProfileKeyJob.class);
+
+ private static final String KEY_GROUP_ID = "group_id";
+
+ private final GroupId.V2 groupId;
+
+ GroupV2UpdateSelfProfileKeyJob(@NonNull GroupId.V2 groupId) {
+ this(new Parameters.Builder()
+ .addConstraint(NetworkConstraint.KEY)
+ .setLifespan(TimeUnit.DAYS.toMillis(1))
+ .setMaxAttempts(Parameters.UNLIMITED)
+ .setQueue(QUEUE)
+ .build(),
+ groupId);
+ }
+
+ private GroupV2UpdateSelfProfileKeyJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId) {
+ super(parameters);
+ this.groupId = groupId;
+ }
+
+ @Override
+ public @NonNull Data serialize() {
+ return new Data.Builder().putString(KEY_GROUP_ID, groupId.toString())
+ .build();
+ }
+
+ @Override
+ public @NonNull String getFactoryKey() {
+ return KEY;
+ }
+
+ @Override
+ public void onRun()
+ throws IOException, GroupNotAMemberException, GroupChangeFailedException, GroupInsufficientRightsException, GroupChangeBusyException
+ {
+ Log.i(TAG, "Updating profile key on group " + groupId);
+ GroupManager.updateSelfProfileKeyInGroup(context, groupId);
+ }
+
+ @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 GroupV2UpdateSelfProfileKeyJob create(@NonNull Parameters parameters, @NonNull Data data) {
+ return new GroupV2UpdateSelfProfileKeyJob(parameters,
+ GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2());
+ }
+ }
+}
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 4f1cbdc45c..f8d235af7b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -20,12 +20,12 @@ import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdFollowUpJobMi
import org.thoughtcrime.securesms.jobmanager.migrations.RecipientIdJobMigration;
import org.thoughtcrime.securesms.jobmanager.migrations.SendReadReceiptsJobMigration;
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
-import org.thoughtcrime.securesms.migrations.PassingMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
+import org.thoughtcrime.securesms.migrations.PassingMigrationJob;
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
import org.thoughtcrime.securesms.migrations.RegistrationPinV2MigrationJob;
import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob;
@@ -91,6 +91,7 @@ public final class JobManagerFactories {
put(ResumableUploadSpecJob.KEY, new ResumableUploadSpecJob.Factory());
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory());
+ put(GroupV2UpdateSelfProfileKeyJob.KEY, new GroupV2UpdateSelfProfileKeyJob.Factory());
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());
put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java
index 438fda5c07..3c02645124 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java
@@ -12,6 +12,8 @@ import androidx.annotation.Nullable;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
+import org.signal.zkgroup.VerificationFailedException;
+import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
@@ -44,7 +46,10 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.BadGroupIdException;
+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.GroupV1MessageProcessor;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
@@ -76,6 +81,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.Base64;
+import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.GroupUtil;
import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil;
@@ -84,12 +90,15 @@ import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
+import org.whispersystems.signalservice.api.groupsv2.NoCredentialForRedemptionTimeException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceContent;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
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.messages.SignalServiceReceiptMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
@@ -110,6 +119,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage
import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMessage;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import java.io.IOException;
import java.security.SecureRandom;
@@ -180,6 +190,7 @@ public final class PushProcessMessageJob extends BaseJob {
this(new Parameters.Builder()
.setQueue(QUEUE)
.setMaxAttempts(Parameters.UNLIMITED)
+ // TODO [Alan] GV2 add network constraint and split queues.
.build(),
messageState,
serializedPlaintextContent,
@@ -234,7 +245,7 @@ public final class PushProcessMessageJob extends BaseJob {
}
@Override
- public void onRun() {
+ public void onRun() throws Exception {
Optional optionalSmsMessageId = smsMessageId > 0 ? Optional.of(smsMessageId) : Optional.absent();
if (messageState == MessageState.DECRYPTED_OK) {
@@ -258,15 +269,19 @@ public final class PushProcessMessageJob extends BaseJob {
}
@Override
- public boolean onShouldRetry(@NonNull Exception exception) {
- return false;
+ public boolean onShouldRetry(@NonNull Exception e) {
+ return e instanceof PushNetworkException ||
+ e instanceof NoCredentialForRedemptionTimeException ||
+ e instanceof GroupChangeBusyException;
}
@Override
public void onFailure() {
}
- private void handleMessage(@Nullable SignalServiceContent content, @NonNull Optional smsMessageId) {
+ private void handleMessage(@Nullable SignalServiceContent content, @NonNull Optional smsMessageId)
+ throws VerificationFailedException, IOException, InvalidGroupStateException, GroupChangeBusyException
+ {
try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
@@ -282,14 +297,25 @@ public final class PushProcessMessageJob extends BaseJob {
boolean isGv2Message = groupId.isPresent() && groupId.get().isV2();
if (isGv2Message) {
- Log.w(TAG, "Ignoring GV2 message.");
- return;
+ GroupMasterKey groupMasterKey = message.getGroupContext().get().getGroupV2().get().getMasterKey();
+
+ if (!groupV2PreProcessMessage(content, groupMasterKey, message.getGroupContext().get().getGroupV2().get())) {
+ Log.i(TAG, "Ignoring GV2 message for group we are not currently in " + groupId);
+ return;
+ }
+
+ GroupId.V2 groupIdV2 = groupId.get().requireV2();
+ Recipient sender = Recipient.externalPush(context, content.getSender());
+ if (!groupDatabase.isCurrentMember(groupIdV2, sender.getId())) {
+ Log.i(TAG, "Ignoring GV2 message from member not in group " + groupId);
+ return;
+ }
}
if (isInvalidMessage(message)) handleInvalidMessage(content.getSender(), content.getSenderDevice(), groupId, content.getTimestamp(), smsMessageId);
else if (message.isEndSession()) handleEndSessionMessage(content, smsMessageId);
- else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId);
- else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
+ else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId, groupId.get().requireV1());
+ else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId, groupId);
else if (message.getReaction().isPresent()) handleReaction(content, message);
else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
@@ -351,6 +377,26 @@ public final class PushProcessMessageJob extends BaseJob {
}
}
+ /**
+ * Attempts to update the group to the version mentioned in the message.
+ * If the local version is at least that it will not check the server.
+ *
+ * @return false iff self is not a current member of the group.
+ */
+ private boolean groupV2PreProcessMessage(@NonNull SignalServiceContent content,
+ @NonNull GroupMasterKey groupMasterKey,
+ @NonNull SignalServiceGroupV2 groupV2)
+ throws IOException, GroupChangeBusyException
+ {
+ try {
+ GroupManager.updateGroupFromServer(context, groupMasterKey, groupV2.getRevision(), content.getTimestamp());
+ return true;
+ } catch (GroupNotAMemberException e) {
+ Log.w(TAG, "Ignoring message for a group we're not in");
+ return false;
+ }
+ }
+
private void handleExceptionMessage(@NonNull ExceptionMetadata e, @NonNull Optional smsMessageId) {
switch (messageState) {
@@ -551,13 +597,14 @@ public final class PushProcessMessageJob extends BaseJob {
private void handleGroupV1Message(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
- @NonNull Optional smsMessageId)
+ @NonNull Optional smsMessageId,
+ @NonNull GroupId.V1 groupId)
throws StorageFailedException, BadGroupIdException
{
GroupV1MessageProcessor.process(context, content, message, false);
if (message.getExpiresInSeconds() != 0 && message.getExpiresInSeconds() != getMessageDestination(content, message).getExpireMessages()) {
- handleExpirationUpdate(content, message, Optional.absent());
+ handleExpirationUpdate(content, message, Optional.absent(), Optional.of(groupId));
}
if (smsMessageId.isPresent()) {
@@ -583,32 +630,46 @@ public final class PushProcessMessageJob extends BaseJob {
private void handleExpirationUpdate(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message,
- @NonNull Optional smsMessageId)
+ @NonNull Optional smsMessageId,
+ @NonNull Optional groupId)
throws StorageFailedException, BadGroupIdException
{
+ if (groupId.isPresent() && groupId.get().isV2()) {
+ Log.w(TAG, "Expiration update received for GV2. Ignoring.");
+ return;
+ }
+
+ int expiresInSeconds = message.getExpiresInSeconds();
+ Optional groupContext = message.getGroupContext();
+ Recipient recipient = getMessageDestination(content, groupContext);
+
+ if (recipient.getExpireMessages() == expiresInSeconds) {
+ Log.i(TAG, "No change in message expiry for group. Ignoring.");
+ return;
+ }
+
try {
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
Recipient sender = Recipient.externalPush(context, content.getSender());
- Recipient recipient = getMessageDestination(content, message);
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(sender.getId(),
- message.getTimestamp(),
+ content.getTimestamp(),
content.getServerTimestamp(),
-1,
- message.getExpiresInSeconds() * 1000L,
- true,
+ expiresInSeconds * 1000L,
+ true,
false,
content.isNeedsReceipt(),
Optional.absent(),
- message.getGroupContext(),
+ groupContext,
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent(),
Optional.absent());
- database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
+ database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
- DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient.getId(), message.getExpiresInSeconds());
+ DatabaseFactory.getRecipientDatabase(context).setExpireMessages(recipient.getId(), expiresInSeconds);
if (smsMessageId.isPresent()) {
DatabaseFactory.getSmsDatabase(context).deleteMessage(smsMessageId.get());
@@ -772,7 +833,7 @@ public final class PushProcessMessageJob extends BaseJob {
private void handleSynchronizeSentMessage(@NonNull SignalServiceContent content,
@NonNull SentTranscriptMessage message)
- throws StorageFailedException, BadGroupIdException
+ throws StorageFailedException, BadGroupIdException, VerificationFailedException, IOException, InvalidGroupStateException
{
try {
GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
@@ -799,8 +860,7 @@ public final class PushProcessMessageJob extends BaseJob {
threadId = handleSynchronizeSentTextMessage(message);
}
- if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) {
- handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get());
+ if (message.getMessage().getGroupContext().isPresent() && groupDatabase.isUnknownGroup(GroupUtil.idFromGroupContext(message.getMessage().getGroupContext().get()))) { handleUnknownGroupMessage(content, message.getMessage().getGroupContext().get());
}
if (message.getMessage().getProfileKey().isPresent()) {
@@ -1112,7 +1172,7 @@ public final class PushProcessMessageJob extends BaseJob {
Recipient recipient = getMessageDestination(content, message);
if (message.getExpiresInSeconds() != recipient.getExpireMessages()) {
- handleExpirationUpdate(content, message, Optional.absent());
+ handleExpirationUpdate(content, message, Optional.absent(), groupId);
}
Long threadId;
@@ -1573,16 +1633,21 @@ public final class PushProcessMessageJob extends BaseJob {
private Recipient getSyncMessageDestination(@NonNull SentTranscriptMessage message)
throws BadGroupIdException
{
- return getGroupRecipient(message.getMessage().getGroupContext())
- .or(() -> Recipient.externalPush(context, message.getDestination().get()));
+ return getGroupRecipient(message.getMessage().getGroupContext()).or(() -> Recipient.externalPush(context, message.getDestination().get()));
}
private Recipient getMessageDestination(@NonNull SignalServiceContent content,
@NonNull SignalServiceDataMessage message)
throws BadGroupIdException
{
- return getGroupRecipient(message.getGroupContext())
- .or(() -> Recipient.externalPush(context, content.getSender()));
+ return getGroupRecipient(message.getGroupContext()).or(() -> Recipient.externalPush(context, content.getSender()));
+ }
+
+ private Recipient getMessageDestination(@NonNull SignalServiceContent content,
+ @NonNull Optional groupContext)
+ throws BadGroupIdException
+ {
+ return getGroupRecipient(groupContext).or(() -> Recipient.externalPush(context, content.getSender()));
}
private Optional getGroupRecipient(Optional message)
@@ -1631,11 +1696,18 @@ public final class PushProcessMessageJob extends BaseJob {
boolean isTextMessage = message.getBody().isPresent();
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent();
boolean isExpireMessage = message.isExpirationUpdate();
- boolean isContentMessage = !message.isGroupV1Update() && !isExpireMessage && (isTextMessage || isMediaMessage);
+ boolean isGv2Message = message.isGroupV2Message();
+ boolean isGv2Update = message.isGroupV2Update();
+ boolean isContentMessage = !message.isGroupV1Update() && !isGv2Update && !isExpireMessage && (isTextMessage || isMediaMessage);
boolean isGroupActive = groupId.isPresent() && groupDatabase.isActive(groupId.get());
boolean isLeaveMessage = message.getGroupContext().isPresent() && message.getGroupContext().get().getGroupV1Type() == SignalServiceGroup.Type.QUIT;
- return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage);
+ if (isGv2Message && !FeatureFlags.groupsV2()) {
+ Log.i(TAG, "Ignoring GV2 message by feature flag.");
+ return true;
+ }
+
+ return (isContentMessage && !isGroupActive) || (sender.isBlocked() && !isLeaveMessage && !isGv2Update);
} else {
return sender.isBlocked();
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoJob.java
index d61533d905..ac2a995b3f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RequestGroupV2InfoJob.java
@@ -81,7 +81,7 @@ public final class RequestGroupV2InfoJob extends BaseJob {
return;
}
- GroupManager.updateGroupFromServer(context, groupId, toRevision);
+ GroupManager.updateGroupFromServer(context, group.get().requireV2GroupProperties().getGroupMasterKey(), toRevision, System.currentTimeMillis());
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java
index 85f72f39ad..55e374ba96 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RotateProfileKeyJob.java
@@ -7,18 +7,18 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.StreamDetails;
-import java.util.UUID;
+import java.util.List;
public class RotateProfileKeyJob extends BaseJob {
@@ -55,6 +55,7 @@ public class RotateProfileKeyJob extends BaseJob {
Recipient self = Recipient.self();
recipientDatabase.setProfileKey(self.getId(), profileKey);
+
try (StreamDetails avatarStream = AvatarHelper.getSelfProfileAvatarStream(context)) {
if (FeatureFlags.VERSIONED_PROFILES) {
accountManager.setVersionedProfile(self.getUuid().get(),
@@ -68,6 +69,16 @@ public class RotateProfileKeyJob extends BaseJob {
}
ApplicationDependencies.getJobManager().add(new RefreshAttributesJob());
+
+ updateProfileKeyOnAllV2Groups();
+ }
+
+ private void updateProfileKeyOnAllV2Groups() {
+ List allGv2Groups = DatabaseFactory.getGroupDatabase(context).getAllGroupV2Ids();
+
+ for (GroupId.V2 groupId : allGv2Groups) {
+ ApplicationDependencies.getJobManager().add(new GroupV2UpdateSelfProfileKeyJob(groupId));
+ }
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
index 88dde0aecb..de42e56a7b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java
@@ -60,6 +60,7 @@ public final class FeatureFlags {
private static final String CALLING_PIP = "android.callingPip";
private static final String NEW_GROUP_UI = "android.newGroupUI";
private static final String REACT_WITH_ANY_EMOJI = "android.reactWithAnyEmoji";
+ private static final String GROUPS_V2 = "android.groupsv2";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -109,7 +110,8 @@ public final class FeatureFlags {
*/
private static final Set STICKY = Sets.newHashSet(
PINS_FOR_ALL_LEGACY,
- PINS_FOR_ALL
+ PINS_FOR_ALL,
+ GROUPS_V2
);
/**
@@ -255,6 +257,11 @@ public final class FeatureFlags {
return getBoolean(REACT_WITH_ANY_EMOJI, false);
}
+ /** Groups v2 send and receive. */
+ public static boolean groupsV2() {
+ return org.whispersystems.signalservice.FeatureFlags.ZK_GROUPS && getBoolean(GROUPS_V2, false);
+ }
+
/** Only for rendering debug info. */
public static synchronized @NonNull Map getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java
index 16601935f5..794f80b169 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java
@@ -144,6 +144,16 @@ public class SignalServiceDataMessage {
group.get().getGroupV1().get().getType() != SignalServiceGroup.Type.DELIVER;
}
+ public boolean isGroupV2Message() {
+ return group.isPresent() &&
+ group.get().getGroupV2().isPresent();
+ }
+
+ public boolean isGroupV2Update() {
+ return isGroupV2Message() &&
+ !body.isPresent();
+ }
+
public int getExpiresInSeconds() {
return expiresInSeconds;
}