diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 6ae641c6b7..4f3a91ad66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -103,7 +103,7 @@ public class ConversationUpdateItem extends LinearLayout } if (this.messageRecord != null && messageRecord.isGroupAction()) { - GroupUtil.getDescription(getContext(), messageRecord.getBody()).removeObserver(this); + GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).removeObserver(this); } this.messageRecord = messageRecord; @@ -113,7 +113,7 @@ public class ConversationUpdateItem extends LinearLayout this.sender.observeForever(this); if (this.messageRecord != null && messageRecord.isGroupAction()) { - GroupUtil.getDescription(getContext(), messageRecord.getBody()).addObserver(this); + GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).addObserver(this); } present(messageRecord); @@ -236,7 +236,7 @@ public class ConversationUpdateItem extends LinearLayout sender.removeForeverObserver(this); } if (this.messageRecord != null && messageRecord.isGroupAction()) { - GroupUtil.getDescription(getContext(), messageRecord.getBody()).removeObserver(this); + GroupUtil.getDescription(getContext(), messageRecord.getBody(), messageRecord.isGroupV2()).removeObserver(this); } } 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 6869592069..e1d5792a4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -1137,7 +1137,11 @@ public class MmsDatabase extends MessagingDatabase { MessageGroupContext.GroupV2Properties groupV2Properties = outgoingGroupUpdateMessage.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.addAll(Stream.concat(Stream.of(groupV2Properties.getPendingMembers()), + Stream.of(groupV2Properties.getRemovedMembers())) + .distinct() + .map(recipientDatabase::getOrInsertFromUuid) + .toList()); } members.remove(Recipient.self().getId()); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 3f6bea3157..d60b325cfa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -616,9 +616,14 @@ public class SmsDatabase extends MessagingDatabase { } else if (message.isSecureMessage()) { type |= Types.SECURE_MESSAGE_BIT; } else if (message.isGroup()) { + IncomingGroupUpdateMessage incomingGroupUpdateMessage = (IncomingGroupUpdateMessage) message; + type |= Types.SECURE_MESSAGE_BIT; - if (((IncomingGroupUpdateMessage)message).isUpdate()) type |= Types.GROUP_UPDATE_BIT; - else if (((IncomingGroupUpdateMessage)message).isQuit()) type |= Types.GROUP_QUIT_BIT; + + if (incomingGroupUpdateMessage.isGroupV2()) type |= Types.GROUP_V2_BIT | Types.GROUP_UPDATE_BIT; + else if (incomingGroupUpdateMessage.isUpdate()) type |= Types.GROUP_UPDATE_BIT; + else if (incomingGroupUpdateMessage.isQuit()) type |= Types.GROUP_QUIT_BIT; + } else if (message.isEndSession()) { type |= Types.SECURE_MESSAGE_BIT; type |= Types.END_SESSION_BIT; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java index 934c79e952..45222fe1ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/DisplayRecord.java @@ -17,9 +17,10 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; -import androidx.annotation.NonNull; import android.text.SpannableString; +import androidx.annotation.NonNull; + import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.recipients.Recipient; @@ -111,6 +112,10 @@ public abstract class DisplayRecord { return SmsDatabase.Types.isGroupUpdate(type); } + public boolean isGroupV2() { + return SmsDatabase.Types.isGroupV2(type); + } + public boolean isGroupQuit() { return SmsDatabase.Types.isGroupQuit(type); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 46171104d6..d803f6dcd0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -99,7 +99,7 @@ public abstract class MessageRecord extends DisplayRecord { if (isGroupUpdate() && isOutgoing()) { return new SpannableString(context.getString(R.string.MessageRecord_you_updated_group)); } else if (isGroupUpdate()) { - return new SpannableString(GroupUtil.getDescription(context, getBody()).toString(getIndividualRecipient())); + return new SpannableString(GroupUtil.getDescription(context, getBody(), false).toString(getIndividualRecipient())); } else if (isGroupQuit() && isOutgoing()) { return new SpannableString(context.getString(R.string.MessageRecord_left_group)); } else if (isGroupQuit()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java index e9ba67933c..4ee6c1a245 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV1.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.groups; import android.content.Context; -import android.graphics.Bitmap; import android.net.Uri; import androidx.annotation.NonNull; @@ -30,8 +29,6 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.sms.MessageSender; -import org.thoughtcrime.securesms.util.BitmapUtil; -import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; @@ -154,7 +151,7 @@ final class GroupManagerV1 { static boolean leaveGroup(@NonNull Context context, @NonNull GroupId.V1 groupId) { Recipient groupRecipient = Recipient.externalGroup(context, groupId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); - Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient); + Optional leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient); if (threadId != -1 && leaveMessage.isPresent()) { try { @@ -180,7 +177,7 @@ final class GroupManagerV1 { if (DatabaseFactory.getGroupDatabase(context).isActive(groupId)) { Recipient groupRecipient = Recipient.externalGroup(context, groupId); long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); - Optional leaveMessage = GroupUtil.createGroupLeaveMessage(context, groupRecipient); + Optional leaveMessage = createGroupLeaveMessage(context, groupId, groupRecipient); if (threadId != -1 && leaveMessage.isPresent()) { ApplicationDependencies.getJobManager().add(LeaveGroupJob.create(groupRecipient)); @@ -210,4 +207,32 @@ final class GroupManagerV1 { OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(recipient, System.currentTimeMillis(), expirationTime * 1000L); MessageSender.send(context, outgoingMessage, threadId, false, null); } + + @WorkerThread + private static Optional createGroupLeaveMessage(@NonNull Context context, + @NonNull GroupId.V1 groupId, + @NonNull Recipient groupRecipient) + { + GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); + + if (!groupDatabase.isActive(groupId)) { + Log.w(TAG, "Group has already been left."); + return Optional.absent(); + } + + GroupContext groupContext = GroupContext.newBuilder() + .setId(ByteString.copyFrom(groupId.getDecodedId())) + .setType(GroupContext.Type.QUIT) + .build(); + + return Optional.of(new OutgoingGroupUpdateMessage(groupRecipient, + groupContext, + null, + System.currentTimeMillis(), + 0, + false, + null, + Collections.emptyList(), + Collections.emptyList())); + } } 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 3076610ec2..30b4593b8d 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 @@ -7,13 +7,16 @@ import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedGroupChange; import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupSecretParams; +import org.signal.zkgroup.util.UUIDUtil; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupId; @@ -29,10 +32,14 @@ import org.thoughtcrime.securesms.mms.MmsException; import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage; +import org.thoughtcrime.securesms.sms.IncomingTextMessage; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException; import java.io.IOException; @@ -132,7 +139,14 @@ public final class GroupsV2StateProcessor { return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null); } - GlobalGroupState inputGroupState = queryServer(); + GlobalGroupState inputGroupState; + try { + inputGroupState = queryServer(); + } catch (GroupNotAMemberException e) { + Log.w(TAG, "Unable to query server for group " + groupId + " server says we're not in group, inserting leave message"); + insertGroupLeave(); + throw e; + } AdvanceGroupStateResult advanceGroupStateResult = GroupStateMapper.partiallyAdvanceGroupState(inputGroupState, revision); DecryptedGroup newLocalState = advanceGroupStateResult.getNewGlobalGroupState().getLocalState(); @@ -152,6 +166,49 @@ public final class GroupsV2StateProcessor { return new GroupUpdateResult(GroupState.GROUP_UPDATED, newLocalState); } + private void insertGroupLeave() { + if (!groupDatabase.isActive(groupId)) { + Log.w(TAG, "Group has already been left."); + return; + } + + Recipient groupRecipient = Recipient.externalGroup(context, groupId); + UUID selfUuid = Recipient.self().getUuid().get(); + DecryptedGroup decryptedGroup = groupDatabase.requireGroup(groupId) + .requireV2GroupProperties() + .getDecryptedGroup(); + + DecryptedGroup simulatedGroupState = DecryptedGroupUtil.removeMember(decryptedGroup, selfUuid, decryptedGroup.getVersion() + 1); + DecryptedGroupChange simulatedGroupChange = DecryptedGroupChange.newBuilder() + .setEditor(UuidUtil.toByteString(UuidUtil.UNKNOWN_UUID)) + .setVersion(simulatedGroupState.getVersion()) + .addDeleteMembers(UuidUtil.toByteString(selfUuid)) + .build(); + + DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(masterKey, simulatedGroupState, simulatedGroupChange); + OutgoingGroupUpdateMessage leaveMessage = new OutgoingGroupUpdateMessage(groupRecipient, + decryptedGroupV2Context, + null, + System.currentTimeMillis(), + 0, + false, + null, + Collections.emptyList(), + Collections.emptyList()); + + try { + MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipient); + long id = mmsDatabase.insertMessageOutbox(leaveMessage, threadId, false, null); + mmsDatabase.markAsSent(id, true); + } catch (MmsException e) { + Log.w(TAG, "Failed to insert leave message.", e); + } + + groupDatabase.setActive(groupId, false); + groupDatabase.remove(groupId, Recipient.self().getId()); + } + /** * @return true iff group exists locally and is at least the specified revision. */ @@ -252,17 +309,29 @@ public final class GroupsV2StateProcessor { } private void storeMessage(@NonNull DecryptedGroupV2Context decryptedGroupV2Context, long timestamp) { - try { - MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); - RecipientId recipientId = recipientDatabase.getOrInsertFromGroupId(groupId); - Recipient recipient = Recipient.resolved(recipientId); - OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(recipient, decryptedGroupV2Context, null, timestamp, 0, false, null, Collections.emptyList(), Collections.emptyList()); - long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); - long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); + UUID editor = DecryptedGroupUtil.editorUuid(decryptedGroupV2Context.getChange()); + boolean outgoing = Recipient.self().getUuid().get().equals(editor); - mmsDatabase.markAsSent(messageId, true); - } catch (MmsException e) { - Log.w(TAG, e); + if (outgoing) { + try { + MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context); + RecipientId recipientId = recipientDatabase.getOrInsertFromGroupId(groupId); + Recipient recipient = Recipient.resolved(recipientId); + OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(recipient, decryptedGroupV2Context, null, timestamp, 0, false, null, Collections.emptyList(), Collections.emptyList()); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient); + long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null); + + mmsDatabase.markAsSent(messageId, true); + } catch (MmsException e) { + Log.w(TAG, e); + } + } else { + SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context); + RecipientId sender = Recipient.externalPush(context, editor, null).getId(); + IncomingTextMessage incoming = new IncomingTextMessage(sender, -1, timestamp, timestamp, "", Optional.of(groupId), 0, false); + IncomingGroupUpdateMessage groupMessage = new IncomingGroupUpdateMessage(incoming, decryptedGroupV2Context); + + smsDatabase.insertMessageInbox(groupMessage); } } } 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 4ee39b5c63..4e30534c0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -58,7 +58,6 @@ 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 { @@ -167,10 +166,9 @@ public class PushGroupSendJob extends PushSendJob { List target; - 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 OutgoingGroupUpdateMessage) target = getGroupMessageV2Recipients((OutgoingGroupUpdateMessage) message); - 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 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(); @@ -330,24 +328,20 @@ 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(); + 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(); - } + List members = Stream.of(DatabaseFactory.getGroupDatabase(context) + .getGroupMembers(groupId, GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF)) + .map(Recipient::getId) + .toList(); - private @NonNull List getGroupMessageV2Recipients(@NonNull OutgoingGroupUpdateMessage 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(); + if (members.size() > 0) { + Log.w(TAG, "No destinations found for group message " + groupId + " using current group membership"); + } + + return members; } public static class Factory implements Job.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java b/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java index a2cd22cde2..34307809e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/MessageGroupContext.java @@ -1,19 +1,24 @@ package org.thoughtcrime.securesms.mms; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; -import com.annimon.stream.Stream; - +import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; -import org.signal.zkgroup.util.UUIDUtil; +import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.Base64; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; +import org.whispersystems.signalservice.api.util.UuidUtil; 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.ArrayList; +import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -22,9 +27,10 @@ import java.util.UUID; */ public final class MessageGroupContext { - private final String encodedGroupContext; - private final GroupV1Properties groupV1; - private final GroupV2Properties groupV2; + @NonNull private final String encodedGroupContext; + @NonNull private final GroupProperties group; + @Nullable private final GroupV1Properties groupV1; + @Nullable private final GroupV2Properties groupV2; public MessageGroupContext(@NonNull String encodedGroupContext, boolean v2) throws IOException @@ -33,9 +39,11 @@ public final class MessageGroupContext { if (v2) { this.groupV1 = null; this.groupV2 = new GroupV2Properties(DecryptedGroupV2Context.parseFrom(Base64.decode(encodedGroupContext))); + this.group = groupV2; } else { this.groupV1 = new GroupV1Properties(GroupContext.parseFrom(Base64.decode(encodedGroupContext))); this.groupV2 = null; + this.group = groupV1; } } @@ -43,12 +51,14 @@ public final class MessageGroupContext { this.encodedGroupContext = Base64.encodeBytes(group.toByteArray()); this.groupV1 = new GroupV1Properties(group); this.groupV2 = null; + this.group = groupV1; } public MessageGroupContext(@NonNull DecryptedGroupV2Context group) { this.encodedGroupContext = Base64.encodeBytes(group.toByteArray()); this.groupV1 = null; this.groupV2 = new GroupV2Properties(group); + this.group = groupV2; } public @NonNull GroupV1Properties requireGroupV1Properties() { @@ -73,7 +83,20 @@ public final class MessageGroupContext { return encodedGroupContext; } - public static class GroupV1Properties { + public String getName() { + return group.getName(); + } + + public List getMembersListExcludingSelf() { + return group.getMembersListExcludingSelf(); + } + + interface GroupProperties { + @NonNull String getName(); + @NonNull List getMembersListExcludingSelf(); + } + + public static class GroupV1Properties implements GroupProperties { private final GroupContext groupContext; @@ -92,9 +115,32 @@ public final class MessageGroupContext { public boolean isUpdate() { return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE; } + + @Override + public @NonNull String getName() { + return groupContext.getName(); + } + + @Override + public @NonNull List getMembersListExcludingSelf() { + List membersList = groupContext.getMembersList(); + if (membersList.isEmpty()) { + return Collections.emptyList(); + } else { + LinkedList members = new LinkedList<>(); + + for (GroupContext.Member member : membersList) { + RecipientId recipient = RecipientId.from(UuidUtil.parseOrNull(member.getUuid()), member.getE164()); + if (!Recipient.self().getId().equals(recipient)) { + members.add(recipient); + } + } + return members; + } + } } - public static class GroupV2Properties { + public static class GroupV2Properties implements GroupProperties { private final DecryptedGroupV2Context decryptedGroupV2Context; private final GroupContextV2 groupContext; @@ -126,9 +172,32 @@ public final class MessageGroupContext { return DecryptedGroupUtil.pendingToUuidList(decryptedGroupV2Context.getGroupState().getPendingMembersList()); } + public @NonNull List getRemovedMembers() { + return DecryptedGroupUtil.removedMembersUuidList(decryptedGroupV2Context.getChange()); + } + public boolean isUpdate() { // The group context is only stored on update messages. return true; } + + @Override + public @NonNull String getName() { + return decryptedGroupV2Context.getGroupState().getTitle(); + } + + @Override + public @NonNull List getMembersListExcludingSelf() { + List members = new ArrayList<>(decryptedGroupV2Context.getGroupState().getMembersCount()); + + for (DecryptedMember member : decryptedGroupV2Context.getGroupState().getMembersList()) { + RecipientId recipient = RecipientId.from(UuidUtil.fromByteString(member.getUuid()), null); + if (!Recipient.self().getId().equals(recipient)) { + members.add(recipient); + } + } + + return members; + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java index b2e7e2c2cc..6a6a40fd5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogRepository.java @@ -42,13 +42,11 @@ final class RecipientDialogRepository { this.groupId = groupId; } - @NonNull - RecipientId getRecipientId() { + @NonNull RecipientId getRecipientId() { return recipientId; } - @Nullable - GroupId getGroupId() { + @Nullable GroupId getGroupId() { return groupId; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupUpdateMessage.java b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupUpdateMessage.java index 54a9922c99..3f526638ea 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupUpdateMessage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/IncomingGroupUpdateMessage.java @@ -1,13 +1,24 @@ package org.thoughtcrime.securesms.sms; +import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; +import org.thoughtcrime.securesms.mms.MessageGroupContext; + import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; public final class IncomingGroupUpdateMessage extends IncomingTextMessage { - private final GroupContext groupContext; + private final MessageGroupContext groupContext; public IncomingGroupUpdateMessage(IncomingTextMessage base, GroupContext groupContext, String body) { - super(base, body); + this(base, new MessageGroupContext(groupContext)); + } + + public IncomingGroupUpdateMessage(IncomingTextMessage base, DecryptedGroupV2Context groupV2Context) { + this(base, new MessageGroupContext(groupV2Context)); + } + + public IncomingGroupUpdateMessage(IncomingTextMessage base, MessageGroupContext groupContext) { + super(base, groupContext.getEncodedGroupContext()); this.groupContext = groupContext; } @@ -17,11 +28,15 @@ public final class IncomingGroupUpdateMessage extends IncomingTextMessage { } public boolean isUpdate() { - return groupContext.getType().getNumber() == GroupContext.Type.UPDATE_VALUE; + return groupContext.isV2Group() || groupContext.requireGroupV1Properties().isUpdate(); + } + + public boolean isGroupV2() { + return groupContext.isV2Group(); } public boolean isQuit() { - return groupContext.getType().getNumber() == GroupContext.Type.QUIT_VALUE; + return !groupContext.isV2Group() && groupContext.requireGroupV1Properties().isQuit(); } } 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 ac12908fad..27b86a52be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/GroupUtil.java @@ -6,15 +6,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import com.google.protobuf.ByteString; - import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.groups.BadGroupIdException; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage; +import org.thoughtcrime.securesms.mms.MessageGroupContext; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -23,15 +21,10 @@ 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.util.UuidUtil; import java.io.IOException; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; -import static org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContext; - public final class GroupUtil { private GroupUtil() { @@ -74,34 +67,13 @@ public final class GroupUtil { return Optional.absent(); } - @WorkerThread - public static Optional createGroupLeaveMessage(@NonNull Context context, @NonNull Recipient groupRecipient) { - GroupId encodedGroupId = groupRecipient.requireGroupId(); - GroupDatabase groupDatabase = DatabaseFactory.getGroupDatabase(context); - - if (!groupDatabase.isActive(encodedGroupId)) { - Log.w(TAG, "Group has already been left."); - return Optional.absent(); - } - - ByteString decodedGroupId = ByteString.copyFrom(encodedGroupId.getDecodedId()); - - GroupContext groupContext = GroupContext.newBuilder() - .setId(decodedGroupId) - .setType(GroupContext.Type.QUIT) - .build(); - - return Optional.of(new OutgoingGroupUpdateMessage(groupRecipient, groupContext, null, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList())); - } - - - public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup) { + public static @NonNull GroupDescription getDescription(@NonNull Context context, @Nullable String encodedGroup, boolean isV2) { if (encodedGroup == null) { return new GroupDescription(context, null); } try { - GroupContext groupContext = GroupContext.parseFrom(Base64.decode(encodedGroup)); + MessageGroupContext groupContext = new MessageGroupContext(encodedGroup, isV2); return new GroupDescription(context, groupContext); } catch (IOException e) { Log.w(TAG, e); @@ -129,25 +101,19 @@ 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 MessageGroupContext groupContext; + @Nullable private final List members; - GroupDescription(@NonNull Context context, @Nullable GroupContext groupContext) { + GroupDescription(@NonNull Context context, @Nullable MessageGroupContext groupContext) { this.context = context.getApplicationContext(); this.groupContext = groupContext; - if (groupContext == null || groupContext.getMembersList().isEmpty()) { + if (groupContext == null) { this.members = null; } else { - this.members = new LinkedList<>(); - - for (GroupContext.Member member : groupContext.getMembersList()) { - RecipientId recipientId = RecipientId.from(UuidUtil.parseOrNull(member.getUuid()), member.getE164()); - if (!recipientId.equals(Recipient.self().getId())) { - this.members.add(recipientId); - } - } + List membersList = groupContext.getMembersListExcludingSelf(); + this.members = membersList.isEmpty() ? null : membersList; } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da85f6eb3b..265b6968d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -678,6 +678,7 @@ All media + Unknown Received a message encrypted using an old version of Signal that is no longer supported. Please ask the sender to update to the most recent version and resend the message. You have left the group. You updated the group. @@ -695,6 +696,9 @@ %1$s set the disappearing message timer to %2$s. + You created the group. + Group updated. + You added %1$s. %1$s added %2$s. 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 2a2ba43ece..bead7b2d65 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 @@ -63,12 +63,26 @@ public final class DecryptedGroupUtil { return uuidList; } + public static ArrayList removedMembersUuidList(DecryptedGroupChange groupChange) { + ArrayList uuidList = new ArrayList<>(groupChange.getDeleteMembersCount()); + + for (ByteString member : groupChange.getDeleteMembersList()) { + uuidList.add(toUuid(member)); + } + + return uuidList; + } + public static UUID toUuid(DecryptedMember member) { - return UUIDUtil.deserialize(member.getUuid().toByteArray()); + return toUuid(member.getUuid()); } public static UUID toUuid(DecryptedPendingMember member) { - return UUIDUtil.deserialize(member.getUuid().toByteArray()); + return toUuid(member.getUuid()); + } + + private static UUID toUuid(ByteString member) { + return UUIDUtil.deserialize(member.toByteArray()); } /** @@ -90,6 +104,16 @@ public final class DecryptedGroupUtil { return Optional.absent(); } + public static Optional firstMember(Collection members) { + Iterator iterator = members.iterator(); + + if (iterator.hasNext()) { + return Optional.of(iterator.next()); + } else { + return Optional.absent(); + } + } + public static Optional findPendingByUuid(Collection members, UUID uuid) { ByteString uuidBytes = UuidUtil.toByteString(uuid);