mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Add the ability to migrate GV1 groups to GV2.
Co-authored-by: Alan Evans <alan@signal.org>
This commit is contained in:
committed by
Alan Evans
parent
2d1bf33902
commit
6bb9d27d4e
@@ -468,6 +468,48 @@ public final class GroupDatabase extends Database {
|
||||
notifyConversationListListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates a V1 group to a V2 group.
|
||||
*/
|
||||
public @NonNull GroupId.V2 migrateToV2(@NonNull GroupId.V1 groupIdV1, @NonNull DecryptedGroup decryptedGroup) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
GroupId.V2 groupIdV2 = groupIdV1.deriveV2MigrationGroupId();
|
||||
GroupMasterKey groupMasterKey = groupIdV1.deriveV2MigrationMasterKey();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
GroupRecord record = getGroup(groupIdV1).get();
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(GROUP_ID, groupIdV2.toString());
|
||||
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
|
||||
contentValues.putNull(EXPECTED_V2_ID);
|
||||
|
||||
List<RecipientId> newMembers = Stream.of(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())).map(u -> RecipientId.from(u, null)).toList();
|
||||
newMembers.addAll(Stream.of(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList())).map(u -> RecipientId.from(u, null)).toList());
|
||||
|
||||
if (record.getMembers().size() > newMembers.size() || !newMembers.containsAll(record.getMembers())) {
|
||||
contentValues.put(FORMER_V1_MEMBERS, RecipientId.toSerializedList(record.getMembers()));
|
||||
}
|
||||
|
||||
int updated = db.update(TABLE_NAME, contentValues, GROUP_ID + " = ?", SqlUtil.buildArgs(groupIdV1.toString()));
|
||||
|
||||
if (updated != 1) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
DatabaseFactory.getRecipientDatabase(context).updateGroupId(groupIdV1, groupIdV2);
|
||||
|
||||
update(groupMasterKey, decryptedGroup);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
return groupIdV2;
|
||||
}
|
||||
|
||||
public void update(@NonNull GroupMasterKey groupMasterKey, @NonNull DecryptedGroup decryptedGroup) {
|
||||
update(GroupId.v2(groupMasterKey), decryptedGroup);
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
||||
public abstract long insertMessageOutbox(@NonNull OutgoingMediaMessage message, long threadId, boolean forceSms, int defaultReceiptStatus, @Nullable SmsDatabase.InsertListener insertListener) throws MmsException;
|
||||
public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName);
|
||||
public abstract void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients);
|
||||
|
||||
public abstract boolean deleteMessage(long messageId);
|
||||
abstract void deleteThread(long threadId);
|
||||
|
||||
@@ -414,6 +414,11 @@ public class MmsDatabase extends MessageDatabase {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTransaction(SQLiteDatabase database) {
|
||||
database.endTransaction();
|
||||
@@ -588,7 +593,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
|
||||
private long getThreadIdFor(@NonNull IncomingMediaMessage retrieved) {
|
||||
if (retrieved.getGroupId() != null) {
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(retrieved.getGroupId());
|
||||
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(retrieved.getGroupId());
|
||||
Recipient groupRecipients = Recipient.resolved(groupRecipientId);
|
||||
return DatabaseFactory.getThreadDatabase(context).getThreadIdFor(groupRecipients);
|
||||
} else {
|
||||
|
||||
@@ -40,6 +40,7 @@ public interface MmsSmsColumns {
|
||||
protected static final long INVALID_MESSAGE_TYPE = 6;
|
||||
protected static final long PROFILE_CHANGE_TYPE = 7;
|
||||
protected static final long MISSED_VIDEO_CALL_TYPE = 8;
|
||||
protected static final long GV1_MIGRATION_TYPE = 9;
|
||||
|
||||
protected static final long BASE_INBOX_TYPE = 20;
|
||||
protected static final long BASE_OUTBOX_TYPE = 21;
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
@@ -377,8 +378,8 @@ public class RecipientDatabase extends Database {
|
||||
return getByColumn(EMAIL, email);
|
||||
}
|
||||
|
||||
public @NonNull Optional<RecipientId> getByGroupId(@NonNull String groupId) {
|
||||
return getByColumn(GROUP_ID, groupId);
|
||||
public @NonNull Optional<RecipientId> getByGroupId(@NonNull GroupId groupId) {
|
||||
return getByColumn(GROUP_ID, groupId.toString());
|
||||
|
||||
}
|
||||
|
||||
@@ -554,7 +555,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getOrInsertFromGroupId(@NonNull GroupId groupId) {
|
||||
Optional<RecipientId> existing = getByColumn(GROUP_ID, groupId.toString());
|
||||
Optional<RecipientId> existing = getByGroupId(groupId);
|
||||
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
@@ -604,6 +605,44 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Recipient#externalPossiblyMigratedGroup(Context, GroupId)}.
|
||||
*/
|
||||
public @NonNull RecipientId getOrInsertFromPossiblyMigratedGroupId(@NonNull GroupId groupId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
Optional<RecipientId> existing = getByColumn(GROUP_ID, groupId.toString());
|
||||
|
||||
if (existing.isPresent()) {
|
||||
return existing.get();
|
||||
}
|
||||
|
||||
if (groupId.isV1()) {
|
||||
Optional<RecipientId> v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId());
|
||||
if (v2.isPresent()) {
|
||||
return v2.get();
|
||||
}
|
||||
}
|
||||
|
||||
if (groupId.isV2()) {
|
||||
Optional<GroupDatabase.GroupRecord> v1 = DatabaseFactory.getGroupDatabase(context).getGroupV1ByExpectedV2(groupId.requireV2());
|
||||
if (v1.isPresent()) {
|
||||
return v1.get().getRecipientId();
|
||||
}
|
||||
}
|
||||
|
||||
RecipientId id = getOrInsertFromGroupId(groupId);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
||||
return id;
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public Cursor getBlocked() {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
|
||||
@@ -877,7 +916,7 @@ public class RecipientDatabase extends Database {
|
||||
for (SignalGroupV1Record insert : groupV1Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(insert.getGroupId()));
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(insert.getGroupId()));
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
|
||||
needsRefresh.add(recipient.getId());
|
||||
@@ -891,7 +930,7 @@ public class RecipientDatabase extends Database {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v1orThrow(update.getOld().getGroupId()));
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId()));
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
||||
needsRefresh.add(recipient.getId());
|
||||
@@ -902,7 +941,7 @@ public class RecipientDatabase extends Database {
|
||||
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||
ContentValues values = getValuesForStorageGroupV2(insert);
|
||||
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
||||
Recipient recipient = Recipient.externalGroupExact(context, groupId);
|
||||
|
||||
if (id < 0) {
|
||||
Log.w(TAG, String.format("Recipient %s is already linked to group %s", recipient.getId(), groupId));
|
||||
@@ -934,7 +973,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(masterKey));
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey));
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
||||
needsRefresh.add(recipient.getId());
|
||||
@@ -1155,7 +1194,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
for (GroupId.V2 id : DatabaseFactory.getGroupDatabase(context).getAllGroupV2Ids()) {
|
||||
Recipient recipient = Recipient.externalGroup(context, id);
|
||||
Recipient recipient = Recipient.externalGroupExact(context, id);
|
||||
RecipientId recipientId = recipient.getId();
|
||||
RecipientSettings recipientSettingsForSync = getRecipientSettingsForSync(recipientId);
|
||||
|
||||
@@ -2300,6 +2339,24 @@ public class RecipientDatabase extends Database {
|
||||
databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2
|
||||
* migration.
|
||||
*/
|
||||
void updateGroupId(@NonNull GroupId.V1 v1Id, @NonNull GroupId.V2 v2Id) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(GROUP_ID, v2Id.toString());
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
|
||||
SqlUtil.Query query = SqlUtil.buildTrueUpdateQuery(GROUP_ID + " = ?", SqlUtil.buildArgs(v1Id), values);
|
||||
|
||||
if (update(query, values)) {
|
||||
RecipientId id = getByGroupId(v2Id).get();
|
||||
markDirty(id, DirtyState.UPDATE);
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will update the database with the content values you specified. It will make an intelligent
|
||||
* query such that this will only return true if a row was *actually* updated.
|
||||
|
||||
@@ -739,6 +739,24 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertGroupV1MigrationEvent(@NonNull RecipientId recipientId, long threadId, List<RecipientId> pendingRecipients) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(RECIPIENT_ID, recipientId.serialize());
|
||||
values.put(ADDRESS_DEVICE_ID, 1);
|
||||
values.put(DATE_RECEIVED, System.currentTimeMillis());
|
||||
values.put(DATE_SENT, System.currentTimeMillis());
|
||||
values.put(READ, 1);
|
||||
values.put(TYPE, Types.GV1_MIGRATION_TYPE);
|
||||
values.put(THREAD_ID, threadId);
|
||||
values.put(BODY, RecipientId.toSerializedList(pendingRecipients));
|
||||
|
||||
databaseHelper.getWritableDatabase().insert(TABLE_NAME, null, values);
|
||||
|
||||
notifyConversationListeners(threadId);
|
||||
ApplicationDependencies.getJobManager().add(new TrimThreadJob(threadId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
|
||||
if (message.isJoined()) {
|
||||
@@ -775,7 +793,7 @@ public class SmsDatabase extends MessageDatabase {
|
||||
if (message.getGroupId() == null) {
|
||||
groupRecipient = null;
|
||||
} else {
|
||||
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(message.getGroupId());
|
||||
RecipientId id = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromPossiblyMigratedGroupId(message.getGroupId());
|
||||
groupRecipient = Recipient.resolved(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
@@ -577,6 +576,28 @@ public class ThreadDatabase extends Database {
|
||||
return db.rawQuery(query, null);
|
||||
}
|
||||
|
||||
public @NonNull List<ThreadRecord> getRecentV1Groups(int limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = MESSAGE_COUNT + " != 0 AND " +
|
||||
"(" +
|
||||
GroupDatabase.TABLE_NAME + "." + GroupDatabase.ACTIVE + " = 1 AND " +
|
||||
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY + " IS NULL AND " +
|
||||
GroupDatabase.TABLE_NAME + "." + GroupDatabase.MMS + " = 0" +
|
||||
")";
|
||||
String query = createQuery(where, 0, limit, true);
|
||||
|
||||
List<ThreadRecord> threadRecords = new ArrayList<>();
|
||||
|
||||
try (Reader reader = readerFor(db.rawQuery(query, null))) {
|
||||
ThreadRecord record;
|
||||
|
||||
while ((record = reader.getNext()) != null) {
|
||||
threadRecords.add(record);
|
||||
}
|
||||
}
|
||||
return threadRecords;
|
||||
}
|
||||
|
||||
public Cursor getConversationList() {
|
||||
return getConversationList("0");
|
||||
}
|
||||
@@ -697,7 +718,7 @@ public class ThreadDatabase extends Database {
|
||||
final String query;
|
||||
|
||||
if (pinned) {
|
||||
query = createQuery(where, PINNED + " ASC", offset, limit, false);
|
||||
query = createQuery(where, PINNED + " ASC", offset, limit);
|
||||
} else {
|
||||
query = createQuery(where, offset, limit, false);
|
||||
}
|
||||
@@ -1076,14 +1097,14 @@ public class ThreadDatabase extends Database {
|
||||
pinnedRecipient = Recipient.externalPush(context, pinned.getContact().get());
|
||||
} else if (pinned.getGroupV1Id().isPresent()) {
|
||||
try {
|
||||
pinnedRecipient = Recipient.externalGroup(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
|
||||
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v1Exact(pinned.getGroupV1Id().get()));
|
||||
} catch (BadGroupIdException e) {
|
||||
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e);
|
||||
pinnedRecipient = null;
|
||||
}
|
||||
} else if (pinned.getGroupV2MasterKey().isPresent()) {
|
||||
try {
|
||||
pinnedRecipient = Recipient.externalGroup(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
|
||||
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
|
||||
} catch (InvalidInputException e) {
|
||||
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e);
|
||||
pinnedRecipient = null;
|
||||
@@ -1321,10 +1342,10 @@ public class ThreadDatabase extends Database {
|
||||
private @NonNull String createQuery(@NonNull String where, long offset, long limit, boolean preferPinned) {
|
||||
String orderBy = (preferPinned ? TABLE_NAME + "." + PINNED + " DESC, " : "") + TABLE_NAME + "." + DATE + " DESC";
|
||||
|
||||
return createQuery(where, orderBy, offset, limit, preferPinned);
|
||||
return createQuery(where, orderBy, offset, limit);
|
||||
}
|
||||
|
||||
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit, boolean preferPinned) {
|
||||
private @NonNull String createQuery(@NonNull String where, @NonNull String orderBy, long offset, long limit) {
|
||||
String projection = Util.join(COMBINED_THREAD_RECIPIENT_GROUP_PROJECTION, ",");
|
||||
|
||||
String query =
|
||||
|
||||
Reference in New Issue
Block a user