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 09bea00ad8..91715cc92b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java
@@ -132,6 +132,21 @@ public final class GroupDatabase extends Database {
return Optional.fromNullable(reader.getCurrent());
}
+ /**
+ * Call if you are sure this group should exist.
+ *
+ * Finds group and throws if it cannot.
+ */
+ public @NonNull GroupRecord requireGroup(@NonNull GroupId groupId) {
+ Optional group = getGroup(groupId);
+
+ if (!group.isPresent()) {
+ throw new AssertionError("Group not found");
+ }
+
+ return group.get();
+ }
+
public boolean isUnknownGroup(@NonNull GroupId groupId) {
Optional group = getGroup(groupId);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
index 66c53931f2..ec37bc2b8b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.IncomingMediaMessage;
+import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
@@ -791,7 +792,7 @@ public class MmsDatabase extends MessagingDatabase {
}
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
- return new OutgoingGroupMediaMessage(recipient, body, attachments, timestamp, 0, false, quote, contacts, previews);
+ return new OutgoingGroupMediaMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews);
} else if (Types.isExpirationTimerUpdate(outboxType)) {
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
}
@@ -1050,8 +1051,16 @@ public class MmsDatabase extends MessagingDatabase {
if (forceSms) type |= Types.MESSAGE_FORCE_SMS_BIT;
if (message.isGroup()) {
- if (((OutgoingGroupMediaMessage)message).isGroupUpdate()) type |= Types.GROUP_UPDATE_BIT;
- else if (((OutgoingGroupMediaMessage)message).isGroupQuit()) type |= Types.GROUP_QUIT_BIT;
+ OutgoingGroupMediaMessage outgoingGroupMediaMessage = (OutgoingGroupMediaMessage) message;
+ if (outgoingGroupMediaMessage.isV2Group()) {
+ MessageGroupContext.GroupV2Properties groupV2Properties = outgoingGroupMediaMessage.requireGroupV2Properties();
+ type |= Types.GROUP_V2_BIT;
+ if (groupV2Properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT;
+ } else {
+ MessageGroupContext.GroupV1Properties properties = outgoingGroupMediaMessage.requireGroupV1Properties();
+ if (properties.isUpdate()) type |= Types.GROUP_UPDATE_BIT;
+ else if (properties.isQuit()) type |= Types.GROUP_QUIT_BIT;
+ }
}
if (message.isExpirationUpdate()) {
@@ -1090,11 +1099,24 @@ public class MmsDatabase extends MessagingDatabase {
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener);
if (message.getRecipient().isGroup()) {
- GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
- List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
+ OutgoingGroupMediaMessage outgoingGroupMediaMessage = (message instanceof OutgoingGroupMediaMessage) ? (OutgoingGroupMediaMessage) message : null;
- receiptDatabase.insert(Stream.of(members).map(Recipient::getId).toList(),
- messageId, defaultReceiptStatus, message.getSentTimeMillis());
+ GroupReceiptDatabase receiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context);
+ RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
+ Set members = new HashSet<>();
+
+ if (outgoingGroupMediaMessage != null && outgoingGroupMediaMessage.isV2Group()) {
+ MessageGroupContext.GroupV2Properties groupV2Properties = outgoingGroupMediaMessage.requireGroupV2Properties();
+ members.addAll(Stream.of(groupV2Properties.getActiveMembers()).map(recipientDatabase::getOrInsertFromUuid).toList());
+ if (groupV2Properties.isUpdate()) {
+ members.addAll(Stream.of(groupV2Properties.getPendingMembers()).map(recipientDatabase::getOrInsertFromUuid).toList());
+ }
+ members.remove(Recipient.self().getId());
+ } else {
+ members.addAll(Stream.of(DatabaseFactory.getGroupDatabase(context).getGroupMembers(message.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)).map(Recipient::getId).toList());
+ }
+
+ receiptDatabase.insert(members, messageId, defaultReceiptStatus, message.getSentTimeMillis());
for (RecipientId recipientId : earlyDeliveryReceipts.keySet()) receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_DELIVERED, -1);
for (RecipientId recipientId : earlyReadReceipts.keySet()) receiptDatabase.update(recipientId, messageId, GroupReceiptDatabase.STATUS_READ, -1);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
index ca01455fa7..52bea99579 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
@@ -76,6 +76,7 @@ public interface MmsSmsColumns {
protected static final long GROUP_UPDATE_BIT = 0x10000;
protected static final long GROUP_QUIT_BIT = 0x20000;
protected static final long EXPIRATION_TIMER_UPDATE_BIT = 0x40000;
+ protected static final long GROUP_V2_BIT = 0x80000;
// Encrypted Storage Information XXX
public static final long ENCRYPTION_MASK = 0xFF000000;
@@ -223,6 +224,10 @@ public interface MmsSmsColumns {
return (type & GROUP_UPDATE_BIT) != 0;
}
+ public static boolean isGroupV2(long type) {
+ return (type & GROUP_V2_BIT) != 0;
+ }
+
public static boolean isGroupQuit(long type) {
return (type & GROUP_QUIT_BIT) != 0;
}
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 f95cdb331f..e9d82523db 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java
@@ -8,6 +8,7 @@ import androidx.annotation.WorkerThread;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
+import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.attachments.Attachment;
@@ -26,6 +27,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mms.MessageGroupContext;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@@ -44,15 +46,18 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Preview;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Quote;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
+import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
+import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class PushGroupSendJob extends PushSendJob {
@@ -147,24 +152,25 @@ public class PushGroupSendJob extends PushSendJob {
return;
}
- if (!message.getRecipient().isPushGroup()) {
+ Recipient groupRecipient = message.getRecipient().fresh();
+
+ if (!groupRecipient.isPushGroup()) {
throw new MmsException("Message recipient isn't a group!");
}
try {
log(TAG, "Sending message: " + messageId);
- if (!message.getRecipient().resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) {
- RecipientUtil.shareProfileIfFirstSecureMessage(context, message.getRecipient());
+ if (!groupRecipient.resolve().isProfileSharing() && !database.isGroupQuitMessage(messageId)) {
+ RecipientUtil.shareProfileIfFirstSecureMessage(context, groupRecipient);
}
List target;
- Recipient groupRecipient = message.getRecipient().fresh();
-
- if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient).getId());
- else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).toList();
- else target = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId);
+ if (filterRecipient != null) target = Collections.singletonList(Recipient.resolved(filterRecipient).getId());
+ else if (!existingNetworkFailures.isEmpty()) target = Stream.of(existingNetworkFailures).map(nf -> nf.getRecipientId(context)).toList();
+ else if (groupRecipient.isPushV2Group() && message instanceof OutgoingGroupMediaMessage) target = getGroupMessageV2Recipients((OutgoingGroupMediaMessage) message);
+ else target = getGroupMessageRecipients(groupRecipient.requireGroupId(), messageId);
List results = deliver(message, groupRecipient, target);
List networkFailures = Stream.of(results).filter(SendMessageResult::isNetworkFailure).map(result -> new NetworkFailure(Recipient.externalPush(context, result.getAddress()).getId())).toList();
@@ -260,37 +266,72 @@ public class PushGroupSendJob extends PushSendJob {
.toList();
if (message.isGroup()) {
- OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
- GroupContext groupContext = groupMessage.getGroupContext();
- SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
- SignalServiceGroup.Type type = groupMessage.isGroupQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
- List members = Stream.of(groupContext.getMembersList())
- .map(m -> new SignalServiceAddress(UuidUtil.parseOrNull(m.getUuid()), m.getE164()))
- .toList();
- SignalServiceGroup group = new SignalServiceGroup(type, groupId.getDecodedId(), groupContext.getName(), members, avatar);
- SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
+ OutgoingGroupMediaMessage groupMessage = (OutgoingGroupMediaMessage) message;
+
+ if (groupMessage.isV2Group()) {
+ MessageGroupContext.GroupV2Properties properties = groupMessage.requireGroupV2Properties();
+ GroupContextV2 groupContext = properties.getGroupContext();
+ SignalServiceGroupV2.Builder builder = SignalServiceGroupV2.newBuilder(properties.getGroupMasterKey())
+ .withRevision(groupContext.getRevision());
+
+ ByteString groupChange = groupContext.getGroupChange();
+ if (groupChange != null) {
+ builder.withSignedGroupChange(groupChange.toByteArray());
+ }
+
+ SignalServiceGroupV2 group = builder.build();
+ SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
.withTimestamp(message.getSentTimeMillis())
.withExpiration(groupRecipient.getExpireMessages())
.asGroupMessage(group)
.build();
- return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage);
+ return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage);
+ } else {
+ MessageGroupContext.GroupV1Properties properties = groupMessage.requireGroupV1Properties();
+
+ GroupContext groupContext = properties.getGroupContext();
+ SignalServiceAttachment avatar = attachmentPointers.isEmpty() ? null : attachmentPointers.get(0);
+ SignalServiceGroup.Type type = properties.isQuit() ? SignalServiceGroup.Type.QUIT : SignalServiceGroup.Type.UPDATE;
+ List members = Stream.of(groupContext.getMembersList())
+ .map(m -> new SignalServiceAddress(UuidUtil.parseOrNull(m.getUuid()), m.getE164()))
+ .toList();
+ SignalServiceGroup group = new SignalServiceGroup(type, groupId.getDecodedId(), groupContext.getName(), members, avatar);
+ SignalServiceDataMessage groupDataMessage = SignalServiceDataMessage.newBuilder()
+ .withTimestamp(message.getSentTimeMillis())
+ .withExpiration(message.getRecipient().getExpireMessages())
+ .asGroupMessage(group)
+ .build();
+
+ return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupDataMessage);
+ }
} else {
- SignalServiceGroup group = new SignalServiceGroup(groupId.getDecodedId());
- SignalServiceDataMessage groupMessage = SignalServiceDataMessage.newBuilder()
- .withTimestamp(message.getSentTimeMillis())
- .asGroupMessage(group)
- .withAttachments(attachmentPointers)
- .withBody(message.getBody())
- .withExpiration((int)(message.getExpiresIn() / 1000))
- .withViewOnce(message.isViewOnce())
- .asExpirationUpdate(message.isExpirationUpdate())
- .withProfileKey(profileKey.orNull())
- .withQuote(quote.orNull())
- .withSticker(sticker.orNull())
- .withSharedContacts(sharedContacts)
- .withPreviews(previews)
- .build();
+ SignalServiceDataMessage.Builder builder = SignalServiceDataMessage.newBuilder()
+ .withTimestamp(message.getSentTimeMillis());
+
+ if (groupId.isV2()) {
+ GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context);
+ GroupDatabase.GroupRecord groupRecord = groupDatabase.requireGroup(groupId);
+ GroupDatabase.V2GroupProperties v2GroupProperties = groupRecord.requireV2GroupProperties();
+ SignalServiceGroupV2 group = SignalServiceGroupV2.newBuilder(v2GroupProperties.getGroupMasterKey())
+ .withRevision(v2GroupProperties.getGroupRevision())
+ .build();
+ builder.asGroupMessage(group);
+ } else {
+ builder.asGroupMessage(new SignalServiceGroup(groupId.getDecodedId()));
+ }
+
+ SignalServiceDataMessage groupMessage = builder.withAttachments(attachmentPointers)
+ .withBody(message.getBody())
+ .withExpiration((int)(message.getExpiresIn() / 1000))
+ .withViewOnce(message.isViewOnce())
+ .asExpirationUpdate(message.isExpirationUpdate())
+ .withProfileKey(profileKey.orNull())
+ .withQuote(quote.orNull())
+ .withSticker(sticker.orNull())
+ .withSharedContacts(sharedContacts)
+ .withPreviews(previews)
+ .build();
return messageSender.sendMessage(addresses, unidentifiedAccess, isRecipientUpdate, groupMessage);
}
@@ -298,12 +339,27 @@ public class PushGroupSendJob extends PushSendJob {
private @NonNull List getGroupMessageRecipients(@NonNull GroupId groupId, long messageId) {
List destinations = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageId);
+
if (!destinations.isEmpty()) return Stream.of(destinations).map(GroupReceiptInfo::getRecipientId).toList();
List members = DatabaseFactory.getGroupDatabase(context).getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
return Stream.of(members).map(Recipient::getId).toList();
}
+ private @NonNull List getGroupMessageV2Recipients(@NonNull OutgoingGroupMediaMessage message) {
+ UUID selfUUId = Recipient.self().getUuid().get();
+ MessageGroupContext.GroupV2Properties groupV2Properties = message.requireGroupV2Properties();
+ boolean includePending = groupV2Properties.isUpdate();
+
+ return Stream.concat(Stream.of(groupV2Properties.getActiveMembers()),
+ includePending ? Stream.of(groupV2Properties.getPendingMembers()) : Stream.empty())
+ .filterNot(selfUUId::equals)
+ .distinct()
+ .map(uuid -> Recipient.externalPush(context, uuid, null))
+ .map(Recipient::getId)
+ .toList();
+ }
+
public static class Factory implements Job.Factory {
@Override
public @NonNull PushGroupSendJob create(@NonNull Parameters parameters, @NonNull org.thoughtcrime.securesms.jobmanager.Data data) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java b/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java
new file mode 100644
index 0000000000..a2cd22cde2
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java
@@ -0,0 +1,134 @@
+package org.thoughtcrime.securesms.mms;
+
+import androidx.annotation.NonNull;
+
+import com.annimon.stream.Stream;
+
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.groups.GroupMasterKey;
+import org.signal.zkgroup.util.UUIDUtil;
+import org.thoughtcrime.securesms.util.Base64;
+import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
+import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
+import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
+import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents either a GroupV1 or GroupV2 encoded context.
+ */
+public final class MessageGroupContext {
+
+ private final String encodedGroupContext;
+ private final GroupV1Properties groupV1;
+ private final GroupV2Properties groupV2;
+
+ public MessageGroupContext(@NonNull String encodedGroupContext, boolean v2)
+ throws IOException
+ {
+ this.encodedGroupContext = encodedGroupContext;
+ if (v2) {
+ this.groupV1 = null;
+ this.groupV2 = new GroupV2Properties(DecryptedGroupV2Context.parseFrom(Base64.decode(encodedGroupContext)));
+ } else {
+ this.groupV1 = new GroupV1Properties(GroupContext.parseFrom(Base64.decode(encodedGroupContext)));
+ this.groupV2 = null;
+ }
+ }
+
+ public MessageGroupContext(@NonNull GroupContext group) {
+ this.encodedGroupContext = Base64.encodeBytes(group.toByteArray());
+ this.groupV1 = new GroupV1Properties(group);
+ this.groupV2 = null;
+ }
+
+ public MessageGroupContext(@NonNull DecryptedGroupV2Context group) {
+ this.encodedGroupContext = Base64.encodeBytes(group.toByteArray());
+ this.groupV1 = null;
+ this.groupV2 = new GroupV2Properties(group);
+ }
+
+ public @NonNull GroupV1Properties requireGroupV1Properties() {
+ if (groupV1 == null) {
+ throw new AssertionError();
+ }
+ return groupV1;
+ }
+
+ public @NonNull GroupV2Properties requireGroupV2Properties() {
+ if (groupV2 == null) {
+ throw new AssertionError();
+ }
+ return groupV2;
+ }
+
+ public boolean isV2Group() {
+ return groupV2 != null;
+ }
+
+ public @NonNull String getEncodedGroupContext() {
+ return encodedGroupContext;
+ }
+
+ public static class GroupV1Properties {
+
+ private final GroupContext groupContext;
+
+ private GroupV1Properties(GroupContext groupContext) {
+ this.groupContext = groupContext;
+ }
+
+ public @NonNull GroupContext getGroupContext() {
+ return groupContext;
+ }
+
+ public boolean isQuit() {
+ return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
+ }
+
+ public boolean isUpdate() {
+ return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
+ }
+ }
+
+ public static class GroupV2Properties {
+
+ private final DecryptedGroupV2Context decryptedGroupV2Context;
+ private final GroupContextV2 groupContext;
+ private final GroupMasterKey groupMasterKey;
+
+ private GroupV2Properties(DecryptedGroupV2Context decryptedGroupV2Context) {
+ this.decryptedGroupV2Context = decryptedGroupV2Context;
+ this.groupContext = decryptedGroupV2Context.getContext();
+ try {
+ groupMasterKey = new GroupMasterKey(groupContext.getMasterKey().toByteArray());
+ } catch (InvalidInputException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public @NonNull GroupContextV2 getGroupContext() {
+ return groupContext;
+ }
+
+ public @NonNull GroupMasterKey getGroupMasterKey() {
+ return groupMasterKey;
+ }
+
+ public @NonNull List getActiveMembers() {
+ return DecryptedGroupUtil.membersToUuidList(decryptedGroupV2Context.getGroupState().getMembersList());
+ }
+
+ public @NonNull List getPendingMembers() {
+ return DecryptedGroupUtil.pendingToUuidList(decryptedGroupV2Context.getGroupState().getPendingMembersList());
+ }
+
+ public boolean isUpdate() {
+ // The group context is only stored on update messages.
+ return true;
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
index 2fb734f87c..3ff807b157 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/OutgoingGroupMediaMessage.java
@@ -6,21 +6,20 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.ThreadDatabase;
+import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext;
-import java.io.IOException;
-import java.util.LinkedList;
+import java.util.Collections;
import java.util.List;
-public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
+public final class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
- private final GroupContext group;
+ private final MessageGroupContext messageGroupContext;
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
- @NonNull String encodedGroupContext,
+ @NonNull MessageGroupContext groupContext,
@NonNull List avatar,
long sentTimeMillis,
long expiresIn,
@@ -28,12 +27,11 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
@Nullable QuoteModel quote,
@NonNull List contacts,
@NonNull List previews)
- throws IOException
{
- super(recipient, encodedGroupContext, avatar, sentTimeMillis,
+ super(recipient, groupContext.getEncodedGroupContext(), avatar, sentTimeMillis,
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, viewOnce, quote, contacts, previews);
- this.group = GroupContext.parseFrom(Base64.decode(encodedGroupContext));
+ this.messageGroupContext = groupContext;
}
public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
@@ -46,12 +44,20 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
@NonNull List contacts,
@NonNull List previews)
{
- super(recipient, Base64.encodeBytes(group.toByteArray()),
- new LinkedList() {{if (avatar != null) add(avatar);}},
- System.currentTimeMillis(),
- ThreadDatabase.DistributionTypes.CONVERSATION, expireIn, viewOnce, quote, contacts, previews);
+ this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews);
+ }
- this.group = group;
+ public OutgoingGroupMediaMessage(@NonNull Recipient recipient,
+ @NonNull DecryptedGroupV2Context group,
+ @Nullable final Attachment avatar,
+ long sentTimeMillis,
+ long expireIn,
+ boolean viewOnce,
+ @Nullable QuoteModel quote,
+ @NonNull List contacts,
+ @NonNull List previews)
+ {
+ this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews);
}
@Override
@@ -59,15 +65,19 @@ public class OutgoingGroupMediaMessage extends OutgoingSecureMediaMessage {
return true;
}
- public boolean isGroupUpdate() {
- return group.getType().getNumber() == GroupContext.Type.UPDATE_VALUE;
+ public boolean isV2Group() {
+ return messageGroupContext.isV2Group();
}
- public boolean isGroupQuit() {
- return group.getType().getNumber() == GroupContext.Type.QUIT_VALUE;
+ public @NonNull MessageGroupContext.GroupV1Properties requireGroupV1Properties() {
+ return messageGroupContext.requireGroupV1Properties();
}
- public GroupContext getGroupContext() {
- return group;
+ public @NonNull MessageGroupContext.GroupV2Properties requireGroupV2Properties() {
+ return messageGroupContext.requireGroupV2Properties();
+ }
+
+ private static List getAttachments(@Nullable Attachment avatar) {
+ return avatar == null ? Collections.emptyList() : Collections.singletonList(avatar);
}
}
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 e09fa8a4f9..df3a4f2914 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java
@@ -575,6 +575,11 @@ public class Recipient {
return groupId != null && groupId.isPush();
}
+ public boolean isPushV2Group() {
+ GroupId groupId = resolve().groupId;
+ return groupId != null && groupId.isV2();
+ }
+
public @NonNull List getParticipants() {
return new ArrayList<>(participants);
}
diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto
index 83fa88aeec..fcac83d00e 100644
--- a/app/src/main/proto/Database.proto
+++ b/app/src/main/proto/Database.proto
@@ -22,3 +22,13 @@ message ReactionList {
repeated Reaction reactions = 1;
}
+
+
+import "SignalService.proto";
+import "DecryptedGroups.proto";
+
+message DecryptedGroupV2Context {
+ signalservice.GroupContextV2 context = 1;
+ DecryptedGroupChange change = 2;
+ DecryptedGroup groupState = 3;
+}
diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java
index e67e030d79..526f8250cb 100644
--- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java
+++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java
@@ -39,6 +39,16 @@ public final class DecryptedGroupUtil {
return uuidList;
}
+ public static ArrayList membersToUuidList(Collection membersList) {
+ ArrayList uuidList = new ArrayList<>(membersList.size());
+
+ for (DecryptedMember member : membersList) {
+ uuidList.add(toUuid(member));
+ }
+
+ return uuidList;
+ }
+
public static ArrayList pendingToUuidList(Collection membersList) {
ArrayList uuidList = new ArrayList<>(membersList.size());