Signal-Android/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java

701 lines
27 KiB
Java

package org.thoughtcrime.securesms.database;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream;
import com.google.protobuf.InvalidProtocolBufferException;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.groupsv2.DecryptedGroupUtil;
import java.io.Closeable;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
public final class GroupDatabase extends Database {
private static final String TAG = Log.tag(GroupDatabase.class);
static final String TABLE_NAME = "groups";
private static final String ID = "_id";
static final String GROUP_ID = "group_id";
static final String RECIPIENT_ID = "recipient_id";
private static final String TITLE = "title";
private static final String MEMBERS = "members";
private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key";
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";
private static final String AVATAR_RELAY = "avatar_relay";
private static final String AVATAR_DIGEST = "avatar_digest";
private static final String TIMESTAMP = "timestamp";
static final String ACTIVE = "active";
static final String MMS = "mms";
/* V2 Group columns */
/** 32 bytes serialized {@link GroupMasterKey} */
private static final String V2_MASTER_KEY = "master_key";
/** Increments with every change to the group */
private static final String V2_REVISION = "revision";
/** Serialized {@link DecryptedGroup} protobuf */
private static final String V2_DECRYPTED_GROUP = "decrypted_group";
public static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME +
" (" + ID + " INTEGER PRIMARY KEY, " +
GROUP_ID + " TEXT, " +
RECIPIENT_ID + " INTEGER, " +
TITLE + " TEXT, " +
MEMBERS + " TEXT, " +
AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " +
AVATAR_RELAY + " TEXT, " +
TIMESTAMP + " INTEGER, " +
ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " +
MMS + " INTEGER DEFAULT 0, " +
V2_MASTER_KEY + " BLOB, " +
V2_REVISION + " BLOB, " +
V2_DECRYPTED_GROUP + " BLOB);";
public static final String[] CREATE_INDEXS = {
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
"CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ");",
};
private static final String[] GROUP_PROJECTION = {
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS
};
static final List<String> TYPED_GROUP_PROJECTION = Stream.of(GROUP_PROJECTION).map(columnName -> TABLE_NAME + "." + columnName).toList();
public GroupDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public Optional<GroupRecord> getGroup(RecipientId recipientId) {
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, RECIPIENT_ID + " = ?", new String[] {recipientId.serialize()}, null, null, null)) {
if (cursor != null && cursor.moveToNext()) {
return getGroup(cursor);
}
return Optional.absent();
}
}
public Optional<GroupRecord> getGroup(@NonNull GroupId groupId) {
try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, GROUP_ID + " = ?",
new String[] {groupId.toString()},
null, null, null))
{
if (cursor != null && cursor.moveToNext()) {
return getGroup(cursor);
}
return Optional.absent();
}
}
Optional<GroupRecord> getGroup(Cursor cursor) {
Reader reader = new Reader(cursor);
return Optional.fromNullable(reader.getCurrent());
}
public boolean isUnknownGroup(@NonNull GroupId groupId) {
Optional<GroupRecord> group = getGroup(groupId);
if (!group.isPresent()) {
return true;
}
boolean noMetadata = !group.get().hasAvatar() && TextUtils.isEmpty(group.get().getTitle());
boolean noMembers = group.get().getMembers().isEmpty() || (group.get().getMembers().size() == 1 && group.get().getMembers().contains(Recipient.self().getId()));
return noMetadata && noMembers;
}
public Reader getGroupsFilteredByTitle(String constraint, boolean includeInactive) {
String query;
String[] queryArgs;
if (includeInactive) {
query = TITLE + " LIKE ? AND (" + ACTIVE + " = ? OR " + RECIPIENT_ID + " IN (SELECT " + ThreadDatabase.RECIPIENT_ID + " FROM " + ThreadDatabase.TABLE_NAME + "))";
queryArgs = new String[]{"%" + constraint + "%", "1"};
} else {
query = TITLE + " LIKE ? AND " + ACTIVE + " = ?";
queryArgs = new String[]{"%" + constraint + "%", "1"};
}
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, query, queryArgs, null, null, TITLE + " COLLATE NOCASE ASC");
return new Reader(cursor);
}
public GroupId.Mms getOrCreateMmsGroupForMembers(List<RecipientId> members) {
Collections.sort(members);
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {GROUP_ID},
MEMBERS + " = ? AND " + MMS + " = ?",
new String[] {RecipientId.toSerializedList(members), "1"},
null, null, null);
try {
if (cursor != null && cursor.moveToNext()) {
return GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)))
.requireMms();
} else {
GroupId.Mms groupId = GroupId.createMms(new SecureRandom());
create(groupId, members);
return groupId;
}
} finally {
if (cursor != null) cursor.close();
}
}
public List<String> getGroupNamesContainingMember(RecipientId recipientId) {
return Stream.of(getGroupsContainingMember(recipientId))
.map(GroupRecord::getTitle)
.toList();
}
public List<GroupRecord> getGroupsContainingMember(RecipientId recipientId) {
SQLiteDatabase database = databaseHelper.getReadableDatabase();
String table = TABLE_NAME + " INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + RECIPIENT_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID;
String query = MEMBERS + " LIKE ?";
String[] args = new String[]{"%" + recipientId.serialize() + "%"};
String orderBy = ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.DATE + " DESC";
List<GroupRecord> groups = new LinkedList<>();
try (Cursor cursor = database.query(table, null, query, args, null, null, orderBy)) {
while (cursor != null && cursor.moveToNext()) {
List<String> members = Util.split(cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)), ",");
if (members.contains(recipientId.serialize())) {
groups.add(new Reader(cursor).getCurrent());
}
}
}
return groups;
}
public Reader getGroups() {
@SuppressLint("Recycle")
Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null);
return new Reader(cursor);
}
@WorkerThread
public @NonNull List<Recipient> getGroupMembers(@NonNull GroupId groupId, @NonNull MemberSet memberSet) {
if (groupId.isV2()) {
return getGroup(groupId).transform(g -> g.requireV2GroupProperties().getMembers(context, memberSet))
.or(Collections.emptyList());
} else {
List<RecipientId> currentMembers = getCurrentMembers(groupId);
List<Recipient> recipients = new ArrayList<>(currentMembers.size());
for (RecipientId member : currentMembers) {
if (memberSet.includeSelf || !Recipient.resolved(member).isLocalNumber()) {
recipients.add(Recipient.resolved(member));
}
}
return recipients;
}
}
public void create(@NonNull GroupId.V1 groupId,
@Nullable String title,
@NonNull Collection<RecipientId> members,
@Nullable SignalServiceAttachmentPointer avatar,
@Nullable String relay)
{
create(groupId, title, members, avatar, relay, null, null);
}
public void create(@NonNull GroupId.Mms groupId,
@NonNull Collection<RecipientId> members)
{
create(groupId, null, members, null, null, null, null);
}
public void create(@NonNull GroupId.V2 groupId,
@Nullable SignalServiceAttachmentPointer avatar,
@Nullable String relay,
@NonNull GroupMasterKey groupMasterKey,
@NonNull DecryptedGroup groupState)
{
create(groupId, groupState.getTitle(), Collections.emptyList(), avatar, relay, groupMasterKey, groupState);
}
/**
* @param groupMasterKey null for V1, must be non-null for V2 (presence dictates group version).
*/
private void create(@NonNull GroupId groupId,
@Nullable String title,
@NonNull Collection<RecipientId> memberCollection,
@Nullable SignalServiceAttachmentPointer avatar,
@Nullable String relay,
@Nullable GroupMasterKey groupMasterKey,
@Nullable DecryptedGroup groupState)
{
List<RecipientId> members = new ArrayList<>(new HashSet<>(memberCollection));
Collections.sort(members);
ContentValues contentValues = new ContentValues();
contentValues.put(RECIPIENT_ID, DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId).serialize());
contentValues.put(GROUP_ID, groupId.toString());
contentValues.put(TITLE, title);
contentValues.put(MEMBERS, RecipientId.toSerializedList(members));
if (avatar != null) {
contentValues.put(AVATAR_ID, avatar.getId());
contentValues.put(AVATAR_KEY, avatar.getKey());
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull());
} else {
contentValues.put(AVATAR_ID, 0);
}
contentValues.put(AVATAR_RELAY, relay);
contentValues.put(TIMESTAMP, System.currentTimeMillis());
contentValues.put(ACTIVE, 1);
contentValues.put(MMS, groupId.isMms());
if (groupMasterKey != null) {
if (groupState == null) {
throw new AssertionError("V2 master key but no group state");
}
groupId.requireV2();
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
contentValues.put(V2_REVISION, groupState.getVersion());
contentValues.put(V2_DECRYPTED_GROUP, groupState.toByteArray());
contentValues.put(MEMBERS, serializeV2GroupMembers(context, groupState));
} else {
if (groupId.isV2()) {
throw new AssertionError("V2 group id but no master key");
}
}
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, contentValues);
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient.live(groupRecipient).refresh();
notifyConversationListListeners();
}
public void update(@NonNull GroupId.V1 groupId,
@Nullable String title,
@Nullable SignalServiceAttachmentPointer avatar)
{
ContentValues contentValues = new ContentValues();
if (title != null) contentValues.put(TITLE, title);
if (avatar != null) {
contentValues.put(AVATAR_ID, avatar.getId());
contentValues.put(AVATAR_CONTENT_TYPE, avatar.getContentType());
contentValues.put(AVATAR_KEY, avatar.getKey());
contentValues.put(AVATAR_DIGEST, avatar.getDigest().orNull());
} else {
contentValues.put(AVATAR_ID, 0);
}
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
GROUP_ID + " = ?",
new String[] {groupId.toString()});
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient.live(groupRecipient).refresh();
notifyConversationListListeners();
}
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
update(GroupId.v2(groupMasterKey), decryptedGroup);
}
public void update(@NonNull GroupId.V2 groupId, @NonNull DecryptedGroup decryptedGroup) {
String title = decryptedGroup.getTitle();
ContentValues contentValues = new ContentValues();
contentValues.put(TITLE, title);
contentValues.put(V2_REVISION, decryptedGroup.getVersion());
contentValues.put(V2_DECRYPTED_GROUP, decryptedGroup.toByteArray());
contentValues.put(MEMBERS, serializeV2GroupMembers(context, decryptedGroup));
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues,
GROUP_ID + " = ?",
new String[]{ groupId.toString() });
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient.live(groupRecipient).refresh();
notifyConversationListListeners();
}
public void updateTitle(@NonNull GroupId.V1 groupId, String title) {
ContentValues contentValues = new ContentValues();
contentValues.put(TITLE, title);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
new String[] {groupId.toString()});
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient.live(groupRecipient).refresh();
}
/**
* Used to bust the Glide cache when an avatar changes.
*/
public void onAvatarUpdated(@NonNull GroupId.V1 groupId, boolean hasAvatar) {
ContentValues contentValues = new ContentValues(1);
contentValues.put(AVATAR_ID, hasAvatar ? Math.abs(new SecureRandom().nextLong()) : 0);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, GROUP_ID + " = ?",
new String[] {groupId.toString()});
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient.live(groupRecipient).refresh();
}
public void updateMembers(@NonNull GroupId groupId, List<RecipientId> members) {
Collections.sort(members);
ContentValues contents = new ContentValues();
contents.put(MEMBERS, RecipientId.toSerializedList(members));
contents.put(ACTIVE, 1);
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
new String[] {groupId.toString()});
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient.live(groupRecipient).refresh();
}
public void remove(@NonNull GroupId groupId, RecipientId source) {
List<RecipientId> currentMembers = getCurrentMembers(groupId);
currentMembers.remove(source);
ContentValues contents = new ContentValues();
contents.put(MEMBERS, RecipientId.toSerializedList(currentMembers));
databaseHelper.getWritableDatabase().update(TABLE_NAME, contents, GROUP_ID + " = ?",
new String[] {groupId.toString()});
RecipientId groupRecipient = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(groupId);
Recipient.live(groupRecipient).refresh();
}
private List<RecipientId> getCurrentMembers(@NonNull GroupId groupId) {
Cursor cursor = null;
try {
cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] {MEMBERS},
GROUP_ID + " = ?",
new String[] {groupId.toString()},
null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS));
return RecipientId.fromSerializedList(serializedMembers);
}
return new LinkedList<>();
} finally {
if (cursor != null)
cursor.close();
}
}
public boolean isActive(@NonNull GroupId groupId) {
Optional<GroupRecord> record = getGroup(groupId);
return record.isPresent() && record.get().isActive();
}
public void setActive(@NonNull GroupId groupId, boolean active) {
SQLiteDatabase database = databaseHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(ACTIVE, active ? 1 : 0);
database.update(TABLE_NAME, values, GROUP_ID + " = ?", new String[] {groupId.toString()});
}
private static String serializeV2GroupMembers(@NonNull Context context, @NonNull DecryptedGroup decryptedGroup) {
List<RecipientId> groupMembers = new ArrayList<>(decryptedGroup.getMembersCount());
for (DecryptedMember member : decryptedGroup.getMembersList()) {
Recipient recipient = Recipient.externalPush(context, new SignalServiceAddress(UuidUtil.fromByteString(member.getUuid()), null));
groupMembers.add(recipient.getId());
}
Collections.sort(groupMembers);
return RecipientId.toSerializedList(groupMembers);
}
public static class Reader implements Closeable {
private final Cursor cursor;
public Reader(Cursor cursor) {
this.cursor = cursor;
}
public @Nullable GroupRecord getNext() {
if (cursor == null || !cursor.moveToNext()) {
return null;
}
return getCurrent();
}
public @Nullable GroupRecord getCurrent() {
if (cursor == null || cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID)) == null) {
return null;
}
return new GroupRecord(GroupId.parse(cursor.getString(cursor.getColumnIndexOrThrow(GROUP_ID))),
RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID))),
cursor.getString(cursor.getColumnIndexOrThrow(TITLE)),
cursor.getString(cursor.getColumnIndexOrThrow(MEMBERS)),
cursor.getLong(cursor.getColumnIndexOrThrow(AVATAR_ID)),
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_KEY)),
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_CONTENT_TYPE)),
cursor.getString(cursor.getColumnIndexOrThrow(AVATAR_RELAY)),
cursor.getInt(cursor.getColumnIndexOrThrow(ACTIVE)) == 1,
cursor.getBlob(cursor.getColumnIndexOrThrow(AVATAR_DIGEST)),
cursor.getInt(cursor.getColumnIndexOrThrow(MMS)) == 1,
cursor.getBlob(cursor.getColumnIndexOrThrow(V2_MASTER_KEY)),
cursor.getInt(cursor.getColumnIndexOrThrow(V2_REVISION)),
cursor.getBlob(cursor.getColumnIndexOrThrow(V2_DECRYPTED_GROUP)));
}
@Override
public void close() {
if (this.cursor != null)
this.cursor.close();
}
}
public static class GroupRecord {
private final GroupId id;
private final RecipientId recipientId;
private final String title;
private final List<RecipientId> members;
private final long avatarId;
private final byte[] avatarKey;
private final byte[] avatarDigest;
private final String avatarContentType;
private final String relay;
private final boolean active;
private final boolean mms;
@Nullable private final V2GroupProperties v2GroupProperties;
public GroupRecord(@NonNull GroupId id, @NonNull RecipientId recipientId, String title, String members,
long avatarId, byte[] avatarKey, String avatarContentType,
String relay, boolean active, byte[] avatarDigest, boolean mms,
@Nullable byte[] groupMasterKeyBytes, int groupRevision, @Nullable byte[] decryptedGroupBytes)
{
this.id = id;
this.recipientId = recipientId;
this.title = title;
this.avatarId = avatarId;
this.avatarKey = avatarKey;
this.avatarDigest = avatarDigest;
this.avatarContentType = avatarContentType;
this.relay = relay;
this.active = active;
this.mms = mms;
V2GroupProperties v2GroupProperties = null;
if (groupMasterKeyBytes != null && decryptedGroupBytes != null) {
GroupMasterKey groupMasterKey;
try {
groupMasterKey = new GroupMasterKey(groupMasterKeyBytes);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
v2GroupProperties = new V2GroupProperties(groupMasterKey, groupRevision, decryptedGroupBytes);
}
this.v2GroupProperties = v2GroupProperties;
if (!TextUtils.isEmpty(members)) this.members = RecipientId.fromSerializedList(members);
else this.members = new LinkedList<>();
}
public GroupId getId() {
return id;
}
public @NonNull RecipientId getRecipientId() {
return recipientId;
}
public String getTitle() {
return title;
}
public List<RecipientId> getMembers() {
return members;
}
public boolean hasAvatar() {
return avatarId != 0;
}
public long getAvatarId() {
return avatarId;
}
public byte[] getAvatarKey() {
return avatarKey;
}
public byte[] getAvatarDigest() {
return avatarDigest;
}
public String getAvatarContentType() {
return avatarContentType;
}
public String getRelay() {
return relay;
}
public boolean isActive() {
return active;
}
public boolean isMms() {
return mms;
}
public boolean isV2Group() {
return v2GroupProperties != null;
}
public @NonNull V2GroupProperties requireV2GroupProperties() {
if (v2GroupProperties == null) {
throw new AssertionError();
}
return v2GroupProperties;
}
public boolean isAdmin(@NonNull Recipient recipient) {
return isV2Group() && requireV2GroupProperties().isAdmin(recipient);
}
}
public static class V2GroupProperties {
@NonNull private final GroupMasterKey groupMasterKey;
private final int groupRevision;
@NonNull private final byte[] decryptedGroupBytes;
private DecryptedGroup decryptedGroup;
private V2GroupProperties(@NonNull GroupMasterKey groupMasterKey, int groupRevision, @NonNull byte[] decryptedGroup) {
this.groupMasterKey = groupMasterKey;
this.groupRevision = groupRevision;
this.decryptedGroupBytes = decryptedGroup;
}
public @NonNull GroupMasterKey getGroupMasterKey() {
return groupMasterKey;
}
public int getGroupRevision() {
return groupRevision;
}
public @NonNull DecryptedGroup getDecryptedGroup() {
try {
if (decryptedGroup == null) {
decryptedGroup = DecryptedGroup.parseFrom(decryptedGroupBytes);
}
return decryptedGroup;
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
}
public boolean isAdmin(@NonNull Recipient recipient) {
return DecryptedGroupUtil.findMemberByUuid(getDecryptedGroup().getMembersList(), recipient.getUuid().get())
.transform(t -> t.getRole() == Member.Role.ADMINISTRATOR)
.or(false);
}
public List<Recipient> getMembers(@NonNull Context context, @NonNull MemberSet memberSet) {
boolean includeSelf = memberSet.includeSelf;
DecryptedGroup groupV2 = getDecryptedGroup();
UUID selfUuid = Recipient.self().getUuid().get();
List<Recipient> recipients = new ArrayList<>(groupV2.getMembersCount() + groupV2.getPendingMembersCount());
for (UUID uuid : DecryptedGroupUtil.toUuidList(groupV2.getMembersList())) {
if (includeSelf || !selfUuid.equals(uuid)) {
recipients.add(Recipient.externalPush(context, uuid, null));
}
}
if (memberSet.includePending) {
for (UUID uuid : DecryptedGroupUtil.pendingToUuidList(groupV2.getPendingMembersList())) {
if (includeSelf || !selfUuid.equals(uuid)) {
recipients.add(Recipient.externalPush(context, uuid, null));
}
}
}
return recipients;
}
}
public enum MemberSet {
FULL_MEMBERS_INCLUDING_SELF(true, false),
FULL_MEMBERS_EXCLUDING_SELF(false, false),
FULL_MEMBERS_AND_PENDING_INCLUDING_SELF(true, true),
FULL_MEMBERS_AND_PENDING_EXCLUDING_SELF(false, true);
private boolean includeSelf;
private boolean includePending;
MemberSet(boolean includeSelf, boolean includePending) {
this.includeSelf = includeSelf;
this.includePending = includePending;
}
}
}