mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 15:58:40 +00:00
GV2 storage service syncing.
This commit is contained in:
parent
36df3f234f
commit
11d17f7496
@ -64,7 +64,7 @@ public final class GroupDatabase extends Database {
|
||||
|
||||
/* V2 Group columns */
|
||||
/** 32 bytes serialized {@link GroupMasterKey} */
|
||||
private static final String V2_MASTER_KEY = "master_key";
|
||||
public 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 */
|
||||
|
@ -14,6 +14,8 @@ import com.google.android.gms.common.util.ArrayUtils;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
@ -23,6 +25,7 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
|
||||
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
@ -44,6 +47,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
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.StorageId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
@ -125,27 +129,26 @@ public class RecipientDatabase extends Database {
|
||||
STORAGE_SERVICE_ID, DIRTY
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME};
|
||||
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
|
||||
static final String[] TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
.map(columnName -> TABLE_NAME + "." + columnName)
|
||||
.toList().toArray(new String[0]);
|
||||
|
||||
private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
|
||||
new String[] { TABLE_NAME + "." + ID },
|
||||
RECIPIENT_PROJECTION,
|
||||
TYPED_RECIPIENT_PROJECTION,
|
||||
new String[] {
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.VERIFIED + " AS " + IDENTITY_STATUS,
|
||||
IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.IDENTITY_KEY + " AS " + IDENTITY_KEY
|
||||
});
|
||||
|
||||
|
||||
public static final String[] CREATE_INDEXS = new String[] {
|
||||
"CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");",
|
||||
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON " + TABLE_NAME + " (" + GROUP_TYPE + ");",
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
private static final String[] SEARCH_PROJECTION = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, "COALESCE(" + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ") AS " + SEARCH_PROFILE_NAME, "COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ") AS " + SORT_NAME};
|
||||
public static final String[] SEARCH_PROJECTION_NAMES = new String[]{ID, SYSTEM_DISPLAY_NAME, PHONE, EMAIL, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, REGISTERED, SEARCH_PROFILE_NAME, SORT_NAME};
|
||||
static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
|
||||
.map(columnName -> TABLE_NAME + "." + columnName)
|
||||
.toList();
|
||||
|
||||
public enum VibrateState {
|
||||
DEFAULT(0), ENABLED(1), DISABLED(2);
|
||||
|
||||
@ -241,7 +244,7 @@ public class RecipientDatabase extends Database {
|
||||
}
|
||||
|
||||
public enum GroupType {
|
||||
NONE(0), MMS(1), SIGNAL_V1(2);
|
||||
NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3);
|
||||
|
||||
private final int id;
|
||||
|
||||
@ -366,7 +369,11 @@ public class RecipientDatabase extends Database {
|
||||
if (groupId.isMms()) {
|
||||
values.put(GROUP_TYPE, GroupType.MMS.getId());
|
||||
} else {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
if (groupId.isV2()) {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
} else {
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId());
|
||||
}
|
||||
values.put(DIRTY, DirtyState.INSERT.getId());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()));
|
||||
}
|
||||
@ -423,29 +430,46 @@ public class RecipientDatabase extends Database {
|
||||
return DirtyState.CLEAN;
|
||||
}
|
||||
|
||||
public @Nullable RecipientSettings getRecipientSettingsForSync(@NonNull RecipientId id) {
|
||||
String query = TABLE_NAME + "." + ID + " = ?";
|
||||
String[] args = new String[]{id.serialize()};
|
||||
|
||||
List<RecipientSettings> recipientSettingsForSync = getRecipientSettingsForSync(query, args);
|
||||
|
||||
if (recipientSettingsForSync.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (recipientSettingsForSync.size() > 1) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
return recipientSettingsForSync.get(0);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncUpdates() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
return getRecipientSettingsForSync(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncInsertions() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
return getRecipientSettingsForSync(query, args);
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientSettings> getPendingRecipientSyncDeletions() {
|
||||
String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?";
|
||||
String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize() };
|
||||
|
||||
return getRecipientSettings(query, args);
|
||||
return getRecipientSettingsForSync(query, args);
|
||||
}
|
||||
|
||||
public @Nullable RecipientSettings getByStorageId(@NonNull byte[] storageId) {
|
||||
List<RecipientSettings> result = getRecipientSettings(STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) });
|
||||
List<RecipientSettings> result = getRecipientSettingsForSync(TABLE_NAME + "." + STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) });
|
||||
|
||||
if (result.size() > 0) {
|
||||
return result.get(0);
|
||||
@ -481,7 +505,9 @@ public class RecipientDatabase extends Database {
|
||||
public void applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
|
||||
@NonNull Collection<RecordUpdate<SignalContactRecord>> contactUpdates,
|
||||
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
|
||||
@NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates)
|
||||
@NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates,
|
||||
@NonNull Collection<SignalGroupV2Record> groupV2Inserts,
|
||||
@NonNull Collection<RecordUpdate<SignalGroupV2Record>> groupV2Updates)
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
@ -586,6 +612,32 @@ public class RecipientDatabase extends Database {
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
for (SignalGroupV2Record insert : groupV2Inserts) {
|
||||
db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV2(insert));
|
||||
|
||||
GroupId.V2 groupId = GroupId.v2(insert.getMasterKey());
|
||||
Recipient recipient = Recipient.externalGroup(context, groupId);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), insert.isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
for (RecordUpdate<SignalGroupV2Record> update : groupV2Updates) {
|
||||
ContentValues values = getValuesForStorageGroupV2(update.getNew());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
|
||||
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.externalGroup(context, GroupId.v2(update.getOld().getMasterKey()));
|
||||
|
||||
threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived());
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
@ -684,13 +736,29 @@ public class RecipientDatabase extends Database {
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static @NonNull ContentValues getValuesForStorageGroupV2(@NonNull SignalGroupV2Record groupV2) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(GROUP_ID, GroupId.v2(groupV2.getMasterKey()).toString());
|
||||
values.put(GROUP_TYPE, GroupType.SIGNAL_V2.getId());
|
||||
values.put(PROFILE_SHARING, groupV2.isProfileSharingEnabled() ? "1" : "0");
|
||||
values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0");
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw()));
|
||||
values.put(DIRTY, DirtyState.CLEAN.getId());
|
||||
return values;
|
||||
}
|
||||
|
||||
private List<RecipientSettings> getRecipientSettings(@Nullable String query, @Nullable String[] args) {
|
||||
private List<RecipientSettings> getRecipientSettingsForSync(@Nullable String query, @Nullable String[] args) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID;
|
||||
String table = TABLE_NAME + " LEFT OUTER JOIN " + IdentityDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + ID + " = " + IdentityDatabase.TABLE_NAME + "." + IdentityDatabase.RECIPIENT_ID
|
||||
+ " LEFT OUTER JOIN " + GroupDatabase.TABLE_NAME + " ON " + TABLE_NAME + "." + GROUP_ID + " = " + GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID;
|
||||
List<RecipientSettings> out = new ArrayList<>();
|
||||
|
||||
try (Cursor cursor = db.query(table, RECIPIENT_FULL_PROJECTION, query, args, null, null, null)) {
|
||||
String[] columns = ArrayUtils.concat(RECIPIENT_FULL_PROJECTION,
|
||||
new String[]{ GroupDatabase.TABLE_NAME + "." + GroupDatabase.GROUP_ID,
|
||||
GroupDatabase.TABLE_NAME + "." + GroupDatabase.V2_MASTER_KEY });
|
||||
|
||||
try (Cursor cursor = db.query(table, columns, query, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
out.add(getRecipientSettings(context, cursor));
|
||||
}
|
||||
@ -722,10 +790,11 @@ public class RecipientDatabase extends Database {
|
||||
GroupType groupType = GroupType.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(GROUP_TYPE)));
|
||||
byte[] key = Base64.decodeOrThrow(encodedKey);
|
||||
|
||||
if (groupType == GroupType.NONE) {
|
||||
out.put(id, StorageId.forContact(key));
|
||||
} else {
|
||||
out.put(id, StorageId.forGroupV1(key));
|
||||
switch (groupType) {
|
||||
case NONE : out.put(id, StorageId.forContact(key)); break;
|
||||
case SIGNAL_V1 : out.put(id, StorageId.forGroupV1(key)); break;
|
||||
case SIGNAL_V2 : out.put(id, StorageId.forGroupV2(key)); break;
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -771,6 +840,19 @@ public class RecipientDatabase extends Database {
|
||||
String identityKeyRaw = cursor.getString(cursor.getColumnIndexOrThrow(IDENTITY_KEY));
|
||||
int identityStatusRaw = cursor.getInt(cursor.getColumnIndexOrThrow(IDENTITY_STATUS));
|
||||
|
||||
int masterKeyIndex = cursor.getColumnIndex(GroupDatabase.V2_MASTER_KEY);
|
||||
GroupMasterKey groupMasterKey = null;
|
||||
try {
|
||||
if (masterKeyIndex != -1) {
|
||||
byte[] blob = cursor.getBlob(masterKeyIndex);
|
||||
if (blob != null) {
|
||||
groupMasterKey = new GroupMasterKey(blob);
|
||||
}
|
||||
}
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
||||
MaterialColor color;
|
||||
byte[] profileKey = null;
|
||||
byte[] profileKeyCredential = null;
|
||||
@ -805,7 +887,7 @@ public class RecipientDatabase extends Database {
|
||||
|
||||
IdentityDatabase.VerifiedStatus identityStatus = IdentityDatabase.VerifiedStatus.forState(identityStatusRaw);
|
||||
|
||||
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, GroupType.fromId(groupType), blocked, muteUntil,
|
||||
return new RecipientSettings(RecipientId.from(id), uuid, username, e164, email, groupId, groupMasterKey, GroupType.fromId(groupType), blocked, muteUntil,
|
||||
VibrateState.fromId(messageVibrateState),
|
||||
VibrateState.fromId(callVibrateState),
|
||||
Util.uri(messageRingtone), Util.uri(callRingtone),
|
||||
@ -1745,6 +1827,7 @@ public class RecipientDatabase extends Database {
|
||||
private final String e164;
|
||||
private final String email;
|
||||
private final GroupId groupId;
|
||||
private final GroupMasterKey groupMasterKey;
|
||||
private final GroupType groupType;
|
||||
private final boolean blocked;
|
||||
private final long muteUntil;
|
||||
@ -1782,6 +1865,7 @@ public class RecipientDatabase extends Database {
|
||||
@Nullable String e164,
|
||||
@Nullable String email,
|
||||
@Nullable GroupId groupId,
|
||||
@Nullable GroupMasterKey groupMasterKey,
|
||||
@NonNull GroupType groupType,
|
||||
boolean blocked,
|
||||
long muteUntil,
|
||||
@ -1819,6 +1903,7 @@ public class RecipientDatabase extends Database {
|
||||
this.e164 = e164;
|
||||
this.email = email;
|
||||
this.groupId = groupId;
|
||||
this.groupMasterKey = groupMasterKey;
|
||||
this.groupType = groupType;
|
||||
this.blocked = blocked;
|
||||
this.muteUntil = muteUntil;
|
||||
@ -1875,6 +1960,13 @@ public class RecipientDatabase extends Database {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only read populated for sync.
|
||||
*/
|
||||
public @Nullable GroupMasterKey getGroupMasterKey() {
|
||||
return groupMasterKey;
|
||||
}
|
||||
|
||||
public @NonNull GroupType getGroupType() {
|
||||
return groupType;
|
||||
}
|
||||
|
@ -85,7 +85,7 @@ public class StorageForcePushJob extends BaseJob {
|
||||
Map<RecipientId, StorageId> newContactStorageIds = generateContactStorageIds(oldContactStorageIds);
|
||||
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
|
||||
List<SignalStorageRecord> inserts = Stream.of(oldContactStorageIds.keySet())
|
||||
.map(recipientDatabase::getRecipientSettings)
|
||||
.map(recipientDatabase::getRecipientSettingsForSync)
|
||||
.withoutNulls()
|
||||
.map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw(), archivedRecipients))
|
||||
.toList();
|
||||
|
@ -6,13 +6,6 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
@ -23,22 +16,28 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncModels;
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncValidations;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -183,7 +182,7 @@ public class StorageSyncJob extends BaseJob {
|
||||
Log.i(TAG, "[Remote Newer] After resolving the conflict, all changes are local. No remote writes needed.");
|
||||
}
|
||||
|
||||
recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates());
|
||||
recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates(), mergeResult.getLocalGroupV2Inserts(), mergeResult.getLocalGroupV2Updates());
|
||||
storageKeyDatabase.applyStorageSyncUpdates(mergeResult.getLocalUnknownInserts(), mergeResult.getLocalUnknownDeletes());
|
||||
StorageSyncHelper.applyAccountStorageSyncUpdates(context, mergeResult.getLocalAccountUpdate());
|
||||
needsMultiDeviceSync = true;
|
||||
@ -277,7 +276,11 @@ public class StorageSyncJob extends BaseJob {
|
||||
case ManifestRecord.Identifier.Type.GROUPV2_VALUE:
|
||||
RecipientSettings settings = recipientDatabase.getByStorageId(id.getRaw());
|
||||
if (settings != null) {
|
||||
records.add(StorageSyncModels.localToRemoteRecord(settings, archivedRecipients));
|
||||
if (settings.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && settings.getGroupMasterKey() == null) {
|
||||
Log.w(TAG, "Missing master key on gv2 recipient");
|
||||
} else {
|
||||
records.add(StorageSyncModels.localToRemoteRecord(settings, archivedRecipients));
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Missing local recipient model! Type: " + id.getType());
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.SetUtil;
|
||||
@ -26,6 +25,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
||||
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.SignalRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
@ -46,8 +46,6 @@ import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
|
||||
public final class StorageSyncHelper {
|
||||
|
||||
private static final String TAG = Log.tag(StorageSyncHelper.class);
|
||||
@ -116,6 +114,11 @@ public final class StorageSyncHelper {
|
||||
Map<RecipientId, byte[]> storageKeyUpdates = new HashMap<>();
|
||||
|
||||
for (RecipientSettings insert : inserts) {
|
||||
if (insert.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && insert.getGroupMasterKey() == null) {
|
||||
Log.w(TAG, "Missing master key on gv2 recipient");
|
||||
continue;
|
||||
}
|
||||
|
||||
storageInserts.add(StorageSyncModels.localToRemoteRecord(insert, archivedRecipients));
|
||||
|
||||
switch (insert.getGroupType()) {
|
||||
@ -125,6 +128,9 @@ public final class StorageSyncHelper {
|
||||
case SIGNAL_V1:
|
||||
completeIds.add(StorageId.forGroupV1(insert.getStorageId()));
|
||||
break;
|
||||
case SIGNAL_V2:
|
||||
completeIds.add(StorageId.forGroupV2(insert.getStorageId()));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unsupported type!");
|
||||
}
|
||||
@ -154,6 +160,10 @@ public final class StorageSyncHelper {
|
||||
oldId = StorageId.forGroupV1(update.getStorageId());
|
||||
newId = StorageId.forGroupV1(generateKey());
|
||||
break;
|
||||
case SIGNAL_V2:
|
||||
oldId = StorageId.forGroupV2(update.getStorageId());
|
||||
newId = StorageId.forGroupV2(generateKey());
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("Unsupported type!");
|
||||
}
|
||||
@ -226,10 +236,12 @@ public final class StorageSyncHelper {
|
||||
|
||||
List<SignalGroupV1Record> remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
|
||||
List<SignalGroupV1Record> localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
|
||||
|
||||
List<SignalGroupV2Record> remoteOnlyGroupV2 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList();
|
||||
List<SignalGroupV2Record> localOnlyGroupV2 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList();
|
||||
|
||||
// TODO [storage] Handle groupV2 when appropriate
|
||||
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
|
||||
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
|
||||
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
|
||||
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
|
||||
|
||||
List<SignalAccountRecord> remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
|
||||
List<SignalAccountRecord> localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
|
||||
@ -242,11 +254,13 @@ public final class StorageSyncHelper {
|
||||
|
||||
RecordMergeResult<SignalContactRecord> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self()));
|
||||
RecordMergeResult<SignalGroupV1Record> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1));
|
||||
RecordMergeResult<SignalGroupV2Record> groupV2MergeResult = resolveRecordConflict(remoteOnlyGroupV2, localOnlyGroupV2, new GroupV2ConflictMerger(localOnlyGroupV2));
|
||||
RecordMergeResult<SignalAccountRecord> accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0))));
|
||||
|
||||
Set<SignalStorageRecord> remoteInserts = new HashSet<>();
|
||||
remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList());
|
||||
remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList());
|
||||
remoteInserts.addAll(Stream.of(groupV2MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV2).toList());
|
||||
remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList());
|
||||
|
||||
Set<RecordUpdate<SignalStorageRecord>> remoteUpdates = new HashSet<>();
|
||||
@ -256,6 +270,9 @@ public final class StorageSyncHelper {
|
||||
remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew())))
|
||||
.toList());
|
||||
remoteUpdates.addAll(Stream.of(groupV2MergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV2(c.getOld()), SignalStorageRecord.forGroupV2(c.getNew())))
|
||||
.toList());
|
||||
remoteUpdates.addAll(Stream.of(accountMergeResult.remoteUpdates)
|
||||
.map(c -> new RecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew())))
|
||||
.toList());
|
||||
@ -269,6 +286,8 @@ public final class StorageSyncHelper {
|
||||
contactMergeResult.localUpdates,
|
||||
groupV1MergeResult.localInserts,
|
||||
groupV1MergeResult.localUpdates,
|
||||
groupV2MergeResult.localInserts,
|
||||
groupV2MergeResult.localUpdates,
|
||||
new LinkedHashSet<>(remoteOnlyUnknowns),
|
||||
new LinkedHashSet<>(localOnlyUnknowns),
|
||||
accountMergeResult.localUpdates.isEmpty() ? Optional.absent() : Optional.of(accountMergeResult.localUpdates.iterator().next()),
|
||||
@ -449,6 +468,8 @@ public final class StorageSyncHelper {
|
||||
private final Set<RecordUpdate<SignalContactRecord>> localContactUpdates;
|
||||
private final Set<SignalGroupV1Record> localGroupV1Inserts;
|
||||
private final Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
|
||||
private final Set<SignalGroupV2Record> localGroupV2Inserts;
|
||||
private final Set<RecordUpdate<SignalGroupV2Record>> localGroupV2Updates;
|
||||
private final Set<SignalStorageRecord> localUnknownInserts;
|
||||
private final Set<SignalStorageRecord> localUnknownDeletes;
|
||||
private final Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate;
|
||||
@ -461,6 +482,8 @@ public final class StorageSyncHelper {
|
||||
@NonNull Set<RecordUpdate<SignalContactRecord>> localContactUpdates,
|
||||
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
|
||||
@NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
|
||||
@NonNull Set<SignalGroupV2Record> localGroupV2Inserts,
|
||||
@NonNull Set<RecordUpdate<SignalGroupV2Record>> localGroupV2Updates,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownInserts,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
|
||||
@NonNull Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate,
|
||||
@ -472,6 +495,8 @@ public final class StorageSyncHelper {
|
||||
this.localContactUpdates = localContactUpdates;
|
||||
this.localGroupV1Inserts = localGroupV1Inserts;
|
||||
this.localGroupV1Updates = localGroupV1Updates;
|
||||
this.localGroupV2Inserts = localGroupV2Inserts;
|
||||
this.localGroupV2Updates = localGroupV2Updates;
|
||||
this.localUnknownInserts = localUnknownInserts;
|
||||
this.localUnknownDeletes = localUnknownDeletes;
|
||||
this.localAccountUpdate = localAccountUpdate;
|
||||
@ -495,6 +520,14 @@ public final class StorageSyncHelper {
|
||||
public @NonNull Set<RecordUpdate<SignalGroupV1Record>> getLocalGroupV1Updates() {
|
||||
return localGroupV1Updates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalGroupV2Record> getLocalGroupV2Inserts() {
|
||||
return localGroupV2Inserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<RecordUpdate<SignalGroupV2Record>> getLocalGroupV2Updates() {
|
||||
return localGroupV2Updates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getLocalUnknownInserts() {
|
||||
return localUnknownInserts;
|
||||
@ -525,10 +558,12 @@ public final class StorageSyncHelper {
|
||||
|
||||
records.addAll(localContactInserts);
|
||||
records.addAll(localGroupV1Inserts);
|
||||
records.addAll(localGroupV2Inserts);
|
||||
records.addAll(remoteInserts);
|
||||
records.addAll(localUnknownInserts);
|
||||
records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getNew).toList());
|
||||
records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getNew).toList());
|
||||
records.addAll(Stream.of(localGroupV2Updates).map(RecordUpdate::getNew).toList());
|
||||
records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getNew).toList());
|
||||
if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getNew());
|
||||
|
||||
@ -541,6 +576,7 @@ public final class StorageSyncHelper {
|
||||
records.addAll(localUnknownDeletes);
|
||||
records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getOld).toList());
|
||||
records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getOld).toList());
|
||||
records.addAll(Stream.of(localGroupV2Updates).map(RecordUpdate::getOld).toList());
|
||||
records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getOld).toList());
|
||||
records.addAll(remoteDeletes);
|
||||
if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getOld());
|
||||
@ -551,8 +587,8 @@ public final class StorageSyncHelper {
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format(Locale.ENGLISH,
|
||||
"localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d",
|
||||
localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size());
|
||||
"localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localGroupV2Inserts: %d, localGroupV2Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d",
|
||||
localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localGroupV2Inserts.size(), localGroupV2Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,16 @@ package org.thoughtcrime.securesms.storage;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.zkgroup.groups.GroupMasterKey;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
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 org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
|
||||
|
||||
import java.util.Set;
|
||||
@ -30,6 +32,7 @@ public final class StorageSyncModels {
|
||||
switch (settings.getGroupType()) {
|
||||
case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId, archived));
|
||||
case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId, archived));
|
||||
case SIGNAL_V2: return SignalStorageRecord.forGroupV2(localToRemoteGroupV2(settings, rawStorageId, archived));
|
||||
default: throw new AssertionError("Unsupported type!");
|
||||
}
|
||||
}
|
||||
@ -52,11 +55,41 @@ public final class StorageSyncModels {
|
||||
}
|
||||
|
||||
private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set<RecipientId> archived) {
|
||||
if (recipient.getGroupId() == null) {
|
||||
GroupId groupId = recipient.getGroupId();
|
||||
|
||||
if (groupId == null) {
|
||||
throw new AssertionError("Must have a groupId!");
|
||||
}
|
||||
|
||||
return new SignalGroupV1Record.Builder(rawStorageId, recipient.getGroupId().getDecodedId())
|
||||
if (!groupId.isV1()) {
|
||||
throw new AssertionError("Group is not V1");
|
||||
}
|
||||
|
||||
return new SignalGroupV1Record.Builder(rawStorageId, groupId.getDecodedId())
|
||||
.setBlocked(recipient.isBlocked())
|
||||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setArchived(archived.contains(recipient.getId()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static @NonNull SignalGroupV2Record localToRemoteGroupV2(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set<RecipientId> archived) {
|
||||
GroupId groupId = recipient.getGroupId();
|
||||
|
||||
if (groupId == null) {
|
||||
throw new AssertionError("Must have a groupId!");
|
||||
}
|
||||
|
||||
if (!groupId.isV2()) {
|
||||
throw new AssertionError("Group is not V2");
|
||||
}
|
||||
|
||||
GroupMasterKey groupMasterKey = recipient.getGroupMasterKey();
|
||||
|
||||
if (groupMasterKey == null) {
|
||||
throw new AssertionError("Group master key not on recipient record");
|
||||
}
|
||||
|
||||
return new SignalGroupV2Record.Builder(rawStorageId, groupMasterKey)
|
||||
.setBlocked(recipient.isBlocked())
|
||||
.setProfileSharingEnabled(recipient.isProfileSharing())
|
||||
.setArchived(archived.contains(recipient.getId()))
|
||||
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.storage;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.airbnb.lottie.L;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.junit.Before;
|
||||
@ -28,6 +27,7 @@ import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -144,7 +144,7 @@ public final class StorageSyncHelperTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_group_sameAsRemote() {
|
||||
public void resolveConflict_group_v1_sameAsRemote() {
|
||||
SignalGroupV1Record remote1 = groupV1(1, 1, true, false);
|
||||
SignalGroupV1Record local1 = groupV1(2, 1, true, false);
|
||||
|
||||
@ -158,6 +158,22 @@ public final class StorageSyncHelperTest {
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_group_v2_sameAsRemote() {
|
||||
SignalGroupV2Record remote1 = groupV2(1, 2, true, false);
|
||||
SignalGroupV2Record local1 = groupV2(2, 2, true, false);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1));
|
||||
|
||||
SignalGroupV2Record expectedMerge = groupV2(1, 2, true, false);
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertEquals(setOf(update(local1, expectedMerge)), result.getLocalGroupV2Updates());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertTrue(result.getRemoteUpdates().isEmpty());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_contact_sameAsLocal() {
|
||||
@ -176,7 +192,7 @@ public final class StorageSyncHelperTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_group_sameAsLocal() {
|
||||
public void resolveConflict_group_v1_sameAsLocal() {
|
||||
SignalGroupV1Record remote1 = groupV1(1, 1, true, false);
|
||||
SignalGroupV1Record local1 = groupV1(2, 1, true, true);
|
||||
|
||||
@ -190,23 +206,37 @@ public final class StorageSyncHelperTest {
|
||||
assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_group_v2_sameAsLocal() {
|
||||
SignalGroupV2Record remote1 = groupV2(1, 2, true, false);
|
||||
SignalGroupV2Record local1 = groupV2(2, 2, true, true);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1));
|
||||
|
||||
SignalGroupV2Record expectedMerge = groupV2(2, 2, true, true);
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertTrue(result.getLocalGroupV2Updates().isEmpty());
|
||||
assertTrue(result.getRemoteInserts().isEmpty());
|
||||
assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveConflict_unknowns() {
|
||||
SignalStorageRecord account = SignalStorageRecord.forAccount(account(99));
|
||||
SignalStorageRecord remote1 = unknown(3);
|
||||
SignalStorageRecord remote2 = unknown(4);
|
||||
SignalStorageRecord remote3 = SignalStorageRecord.forGroupV2(groupV2(100, 200, true, true));
|
||||
SignalStorageRecord local1 = unknown(1);
|
||||
SignalStorageRecord local2 = unknown(2);
|
||||
SignalStorageRecord local3 = SignalStorageRecord.forGroupV2(groupV2(101, 201, true, true));
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, remote3, account), setOf(local1, local2, local3, account));
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, account), setOf(local1, local2, account));
|
||||
|
||||
assertTrue(result.getLocalContactInserts().isEmpty());
|
||||
assertTrue(result.getLocalContactUpdates().isEmpty());
|
||||
assertEquals(setOf(remote1, remote2, remote3), result.getLocalUnknownInserts());
|
||||
assertEquals(setOf(local1, local2, local3), result.getLocalUnknownDeletes());
|
||||
assertEquals(setOf(remote1, remote2), result.getLocalUnknownInserts());
|
||||
assertEquals(setOf(local1, local2), result.getLocalUnknownDeletes());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
}
|
||||
|
||||
@ -224,29 +254,34 @@ public final class StorageSyncHelperTest {
|
||||
SignalGroupV1Record remote4 = groupV1(7, 1, true, false);
|
||||
SignalGroupV1Record local4 = groupV1(8, 1, false, true);
|
||||
|
||||
SignalAccountRecord remote5 = account(9);
|
||||
SignalAccountRecord local5 = account(10);
|
||||
SignalGroupV2Record remote5 = groupV2(9, 2, true, false);
|
||||
SignalGroupV2Record local5 = groupV2(10, 2, false, true);
|
||||
|
||||
SignalStorageRecord unknownRemote = unknown(11);
|
||||
SignalStorageRecord unknownLocal = unknown(12);
|
||||
SignalAccountRecord remote6 = account(11);
|
||||
SignalAccountRecord local6 = account(12);
|
||||
|
||||
StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222));
|
||||
SignalStorageRecord unknownRemote = unknown(13);
|
||||
SignalStorageRecord unknownLocal = unknown(14);
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, unknownRemote);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, local5, unknownLocal);
|
||||
StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222, 333));
|
||||
|
||||
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, remote6, unknownRemote);
|
||||
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, local5, local6, unknownLocal);
|
||||
|
||||
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly);
|
||||
|
||||
SignalContactRecord merge1 = contact(2, UUID_A, E164_A, "a");
|
||||
SignalContactRecord merge2 = contact(111, UUID_B, E164_B, "b");
|
||||
SignalGroupV1Record merge4 = groupV1(222, 1, true, true);
|
||||
SignalGroupV2Record merge5 = groupV2(333, 2, true, true);
|
||||
|
||||
assertEquals(setOf(remote3), result.getLocalContactInserts());
|
||||
assertEquals(setOf(update(local2, merge2)), result.getLocalContactUpdates());
|
||||
assertEquals(setOf(update(local4, merge4)), result.getLocalGroupV1Updates());
|
||||
assertEquals(setOf(update(local5, merge5)), result.getLocalGroupV2Updates());
|
||||
assertEquals(setOf(SignalStorageRecord.forContact(local3)), result.getRemoteInserts());
|
||||
assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4)), result.getRemoteUpdates());
|
||||
assertEquals(Optional.of(update(local5, remote5)), result.getLocalAccountUpdate());
|
||||
assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4), recordUpdate(remote5, merge5)), result.getRemoteUpdates());
|
||||
assertEquals(Optional.of(update(local6, remote6)), result.getLocalAccountUpdate());
|
||||
assertEquals(setOf(unknownRemote), result.getLocalUnknownInserts());
|
||||
assertEquals(setOf(unknownLocal), result.getLocalUnknownDeletes());
|
||||
assertTrue(result.getRemoteDeletes().isEmpty());
|
||||
@ -254,7 +289,7 @@ public final class StorageSyncHelperTest {
|
||||
|
||||
@Test
|
||||
public void createWriteOperation_generic() {
|
||||
List<StorageId> localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100));
|
||||
List<StorageId> localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100), groupV2Key(200));
|
||||
SignalContactRecord insert1 = contact(6, UUID_A, E164_A, "a");
|
||||
SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b");
|
||||
SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z");
|
||||
@ -264,6 +299,9 @@ public final class StorageSyncHelperTest {
|
||||
SignalGroupV1Record insert3 = groupV1(9, 1, true, true);
|
||||
SignalGroupV1Record old3 = groupV1(100, 1, true, true);
|
||||
SignalGroupV1Record new3 = groupV1(10, 1, false, true);
|
||||
SignalGroupV2Record insert4 = groupV2(19, 2, true, true);
|
||||
SignalGroupV2Record old4 = groupV2(200, 2, true, true);
|
||||
SignalGroupV2Record new4 = groupV2(20, 2, false, true);
|
||||
SignalStorageRecord unknownInsert = unknown(11);
|
||||
SignalStorageRecord unknownDelete = unknown(12);
|
||||
|
||||
@ -273,18 +311,19 @@ public final class StorageSyncHelperTest {
|
||||
setOf(update(old2, new2)),
|
||||
setOf(insert3),
|
||||
setOf(update(old3, new3)),
|
||||
setOf(insert4),
|
||||
setOf(update(old4, new4)),
|
||||
setOf(unknownInsert),
|
||||
setOf(unknownDelete),
|
||||
Optional.absent(),
|
||||
recordSetOf(insert1, insert3),
|
||||
setOf(recordUpdate(old1, new1), recordUpdate(old3, new3)),
|
||||
recordSetOf(insert1, insert3, insert4),
|
||||
setOf(recordUpdate(old1, new1), recordUpdate(old3, new3), recordUpdate(old4, new4)),
|
||||
setOf()));
|
||||
|
||||
assertEquals(2, result.getManifest().getVersion());
|
||||
assertContentsEqual(Arrays.asList(contactKey(3), contactKey(4), contactKey(5), contactKey(6), contactKey(7), contactKey(8), groupV1Key(9), groupV1Key(10), unknownKey(11)), result.getManifest().getStorageIds());
|
||||
assertTrue(recordSetOf(insert1, new1, insert3, new3).containsAll(result.getInserts()));
|
||||
assertEquals(4, result.getInserts().size());
|
||||
assertByteListEquals(byteListOf(1, 100), result.getDeletes());
|
||||
assertContentsEqual(Arrays.asList(contactKey(3), contactKey(4), contactKey(5), contactKey(6), contactKey(7), contactKey(8), groupV1Key(9), groupV1Key(10), groupV2Key(19), groupV2Key(20), unknownKey(11)), result.getManifest().getStorageIds());
|
||||
assertEquals(recordSetOf(insert1, new1, insert3, new3, insert4, new4), new HashSet<>(result.getInserts()));
|
||||
assertByteListEquals(byteListOf(1, 100, 200), result.getDeletes());
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -415,6 +454,10 @@ public final class StorageSyncHelperTest {
|
||||
return StorageId.forGroupV1(byteArray(val));
|
||||
}
|
||||
|
||||
private static StorageId groupV2Key(int val) {
|
||||
return StorageId.forGroupV2(byteArray(val));
|
||||
}
|
||||
|
||||
private static StorageId unknownKey(int val) {
|
||||
return StorageId.forType(byteArray(val), UNKNOWN_TYPE);
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ public final class SignalGroupV2Record implements SignalRecord {
|
||||
private final GroupV2Record.Builder builder;
|
||||
|
||||
public Builder(byte[] rawId, GroupMasterKey masterKey) {
|
||||
this.id = StorageId.forGroupV1(rawId);
|
||||
this.id = StorageId.forGroupV2(rawId);
|
||||
this.builder = GroupV2Record.newBuilder();
|
||||
|
||||
builder.setMasterKey(ByteString.copyFrom(masterKey.serialize()));
|
||||
|
@ -7,7 +7,7 @@ import java.util.Objects;
|
||||
|
||||
public class SignalStorageRecord implements SignalRecord {
|
||||
|
||||
private final StorageId id;
|
||||
private final StorageId id;
|
||||
private final Optional<SignalContactRecord> contact;
|
||||
private final Optional<SignalGroupV1Record> groupV1;
|
||||
private final Optional<SignalGroupV2Record> groupV2;
|
||||
@ -88,7 +88,7 @@ public class SignalStorageRecord implements SignalRecord {
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return !contact.isPresent() && !groupV1.isPresent() && !account.isPresent();
|
||||
return !contact.isPresent() && !groupV1.isPresent() && !groupV2.isPresent() && !account.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -97,12 +97,13 @@ public class SignalStorageRecord implements SignalRecord {
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalStorageRecord that = (SignalStorageRecord) o;
|
||||
return Objects.equals(id, that.id) &&
|
||||
Objects.equals(contact, that.contact) &&
|
||||
Objects.equals(groupV1, that.groupV1);
|
||||
Objects.equals(contact, that.contact) &&
|
||||
Objects.equals(groupV1, that.groupV1) &&
|
||||
Objects.equals(groupV2, that.groupV2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, contact, groupV1);
|
||||
return Objects.hash(id, contact, groupV1, groupV2);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user