diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java index 25883ef107..5135addfbc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java @@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; +import org.thoughtcrime.securesms.jobs.StorageSyncJobV2; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.permissions.Permissions; @@ -194,7 +194,7 @@ public class DirectoryHelper { if (newRegisteredState != originalRegisteredState) { ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob()); - ApplicationDependencies.getJobManager().add(StorageSyncJob.create()); + ApplicationDependencies.getJobManager().add(new StorageSyncJobV2()); if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) { notifyNewUsers(context, Collections.singletonList(recipient.getId())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index b6ccf63a0e..ae93257bb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -978,222 +978,7 @@ public class RecipientDatabase extends Database { recipient.live().refresh(); } - public boolean applyStorageSyncUpdates(@NonNull Collection contactInserts, - @NonNull Collection> contactUpdates, - @NonNull Collection groupV1Inserts, - @NonNull Collection> groupV1Updates, - @NonNull Collection groupV2Inserts, - @NonNull Collection> groupV2Updates) - { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context); - ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); - Set needsRefresh = new HashSet<>(); - boolean forcePush = false; - - db.beginTransaction(); - - try { - for (SignalContactRecord insert : contactInserts) { - ContentValues values = getValuesForStorageContact(insert, true); - long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); - RecipientId recipientId = null; - - if (id < 0) { - values = getValuesForStorageContact(insert, false); - Log.w(TAG, "Failed to insert! It's likely that these were newly-registered users that were missed in the merge. Doing an update instead."); - - if (insert.getAddress().getNumber().isPresent()) { - try { - int count = db.update(TABLE_NAME, values, PHONE + " = ?", new String[] { insert.getAddress().getNumber().get() }); - Log.w(TAG, "Updated " + count + " users by E164."); - } catch (SQLiteConstraintException e) { - Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Failed to update the UUID on an existing E164 user. Possibly merging."); - recipientId = getAndPossiblyMerge(insert.getAddress().getUuid().get(), insert.getAddress().getNumber().get(), true); - Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Resulting id: " + recipientId); - } - } - - if (recipientId == null && insert.getAddress().getUuid().isPresent()) { - try { - int count = db.update(TABLE_NAME, values, UUID + " = ?", new String[] { insert.getAddress().getUuid().get().toString() }); - Log.w(TAG, "Updated " + count + " users by UUID."); - } catch (SQLiteConstraintException e) { - Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Failed to update the E164 on an existing UUID user. Possibly merging."); - recipientId = getAndPossiblyMerge(insert.getAddress().getUuid().get(), insert.getAddress().getNumber().get(), true); - Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Resulting id: " + recipientId); - } - } - - if (recipientId == null && insert.getAddress().getNumber().isPresent()) { - recipientId = getByE164(insert.getAddress().getNumber().get()).orNull(); - } - - if (recipientId == null && insert.getAddress().getUuid().isPresent()) { - recipientId = getByUuid(insert.getAddress().getUuid().get()).orNull(); - } - - if (recipientId == null) { - Log.w(TAG, "Failed to recover from a failed insert!"); - continue; - } - } else { - recipientId = RecipientId.from(id); - } - - if (insert.getIdentityKey().isPresent()) { - try { - IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0); - - DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState())); - } catch (InvalidKeyException e) { - Log.w(TAG, "Failed to process identity key during insert! Skipping.", e); - } - } - - threadDatabase.applyStorageSyncUpdate(recipientId, insert); - needsRefresh.add(recipientId); - } - - for (StorageRecordUpdate update : contactUpdates) { - ContentValues values = getValuesForStorageContact(update.getNew(), false); - - try { - 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!"); - } - } catch (SQLiteConstraintException e) { - Log.w(TAG, "[applyStorageSyncUpdates -- Update] Failed to update a user by storageId."); - - RecipientId recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getOld().getId().getRaw())).get(); - Log.w(TAG, "[applyStorageSyncUpdates -- Update] Found user " + recipientId + ". Possibly merging."); - - recipientId = getAndPossiblyMerge(update.getNew().getAddress().getUuid().orNull(), update.getNew().getAddress().getNumber().orNull(), true); - Log.w(TAG, "[applyStorageSyncUpdates -- Update] Merged into " + recipientId); - - db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId)); - } - - RecipientId recipientId = getByStorageKeyOrThrow(update.getNew().getId().getRaw()); - - if (StorageSyncHelper.profileKeyChanged(update)) { - ContentValues clearValues = new ContentValues(1); - clearValues.putNull(PROFILE_KEY_CREDENTIAL); - update(recipientId, clearValues); - } - - try { - Optional oldIdentityRecord = identityDatabase.getIdentity(recipientId); - - if (update.getNew().getIdentityKey().isPresent()) { - IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0); - DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState())); - } - - Optional newIdentityRecord = identityDatabase.getIdentity(recipientId); - - if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) && - (!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED)) - { - IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true); - } else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) && - (oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED)) - { - IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true); - } - } catch (InvalidKeyException e) { - Log.w(TAG, "Failed to process identity key during update! Skipping.", e); - } - - threadDatabase.applyStorageSyncUpdate(recipientId, update.getNew()); - needsRefresh.add(recipientId); - } - - for (SignalGroupV1Record insert : groupV1Inserts) { - long id = db.insertWithOnConflict(TABLE_NAME, null, getValuesForStorageGroupV1(insert), SQLiteDatabase.CONFLICT_IGNORE); - - if (id < 0) { - Log.w(TAG, "Duplicate GV1 entry detected! Ignoring, suggesting force-push."); - forcePush = true; - } else { - Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(insert.getGroupId())); - - threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert); - needsRefresh.add(recipient.getId()); - } - } - - for (StorageRecordUpdate update : groupV1Updates) { - ContentValues values = getValuesForStorageGroupV1(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.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId())); - - threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew()); - needsRefresh.add(recipient.getId()); - } - - for (SignalGroupV2Record insert : groupV2Inserts) { - GroupMasterKey masterKey = insert.getMasterKeyOrThrow(); - GroupId.V2 groupId = GroupId.v2(masterKey); - ContentValues values = getValuesForStorageGroupV2(insert); - long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); - 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)); - } else { - Log.i(TAG, String.format("Inserted recipient %s for group %s", recipient.getId(), groupId)); - } - - Log.i(TAG, "Creating restore placeholder for " + groupId); - DatabaseFactory.getGroupDatabase(context) - .create(masterKey, - DecryptedGroup.newBuilder() - .setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION) - .build()); - - Log.i(TAG, "Scheduling request for latest group info for " + groupId); - - ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId)); - - threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert); - needsRefresh.add(recipient.getId()); - } - - for (StorageRecordUpdate 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!"); - } - - GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow(); - Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey)); - - threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew()); - needsRefresh.add(recipient.getId()); - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - for (RecipientId id : needsRefresh) { - Recipient.live(id).refresh(); - } - - return forcePush; - } - - public void applyStorageSyncUpdates(@NonNull StorageId storageId, SignalAccountRecord update) { + public void applyStorageSyncAccountUpdate(@NonNull StorageId storageId, SignalAccountRecord update) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); ContentValues values = new ContentValues(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java index be2395d28f..5d8b386e91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DirectoryRefreshJob.java @@ -34,7 +34,7 @@ public class DirectoryRefreshJob extends BaseJob { boolean notifyOfNewUsers) { this(new Job.Parameters.Builder() - .setQueue(StorageSyncJob.QUEUE_KEY) + .setQueue(StorageSyncJobV2.QUEUE_KEY) .addConstraint(NetworkConstraint.KEY) .setMaxAttempts(10) .build(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 597df9ac14..ebcd326439 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -141,7 +141,6 @@ public final class JobManagerFactories { put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory()); put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory()); put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory()); - put(StorageSyncJob.KEY, new StorageSyncJob.Factory()); put(StorageSyncJobV2.KEY, new StorageSyncJobV2.Factory()); put(TrimThreadJob.KEY, new TrimThreadJob.Factory()); put(TypingSendJob.KEY, new TypingSendJob.Factory()); @@ -190,6 +189,7 @@ public final class JobManagerFactories { put("Argon2TestJob", new FailingJob.Factory()); put("Argon2TestMigrationJob", new PassingMigrationJob.Factory()); put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory()); + put("StorageSyncJob", new StorageSyncJobV2.Factory()); put("WakeGroupV2Job", new FailingJob.Factory()); }}; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java index b5946875ed..3be4ace3fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java @@ -40,7 +40,7 @@ public class StorageAccountRestoreJob extends BaseJob { public StorageAccountRestoreJob() { this(new Parameters.Builder() - .setQueue(StorageSyncJob.QUEUE_KEY) + .setQueue(StorageSyncJobV2.QUEUE_KEY) .addConstraint(NetworkConstraint.KEY) .setMaxInstancesForFactory(1) .setMaxAttempts(1) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java index 130f8050a7..be2b6668d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -50,7 +50,7 @@ public class StorageForcePushJob extends BaseJob { public StorageForcePushJob() { this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY) - .setQueue(StorageSyncJob.QUEUE_KEY) + .setQueue(StorageSyncJobV2.QUEUE_KEY) .setMaxInstancesForFactory(1) .setLifespan(TimeUnit.DAYS.toMillis(1)) .build()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java deleted file mode 100644 index 90d542bf08..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ /dev/null @@ -1,376 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import android.content.Context; - -import androidx.annotation.NonNull; - -import com.annimon.stream.Stream; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.RecipientDatabase; -import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; -import org.thoughtcrime.securesms.database.UnknownStorageIdDatabase; -import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.groups.GroupId; -import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil; -import org.thoughtcrime.securesms.jobmanager.Data; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.storage.GroupV2ExistenceChecker; -import org.thoughtcrime.securesms.storage.StaticGroupV2ExistenceChecker; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; -import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult; -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.FeatureFlags; -import org.thoughtcrime.securesms.util.GroupUtil; -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.push.exceptions.PushNetworkException; -import org.whispersystems.signalservice.api.storage.SignalAccountRecord; -import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; -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; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -/** - * Does a full sync of our local storage state with the remote storage state. Will write any pending - * local changes and resolve any conflicts with remote storage. - * - * This should be performed whenever a change is made locally, or whenever we want to retrieve - * changes that have been made remotely. - */ -public class StorageSyncJob extends BaseJob { - - public static final String KEY = "StorageSyncJob"; - public static final String QUEUE_KEY = "StorageSyncingJobs"; - - private static final String TAG = Log.tag(StorageSyncJob.class); - - private StorageSyncJob() { - this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY) - .setQueue(QUEUE_KEY) - .setMaxInstancesForFactory(2) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .build()); - } - - private StorageSyncJob(@NonNull Parameters parameters) { - super(parameters); - } - - public static void enqueue() { - if (FeatureFlags.internalUser()) { - ApplicationDependencies.getJobManager().add(new StorageSyncJobV2()); - } else { - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); - } - } - - public static @NonNull Job create() { - if (FeatureFlags.storageSyncV2()) { - return new StorageSyncJobV2(); - } else { - return new StorageSyncJob(); - } - } - - @Override - protected boolean shouldTrace() { - return true; - } - - @Override - public @NonNull Data serialize() { - return Data.EMPTY; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - protected void onRun() throws IOException, RetryLaterException { - if (!SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().hasOptedOut()) { - Log.i(TAG, "Doesn't have a PIN. Skipping."); - return; - } - - if (!TextSecurePreferences.isPushRegistered(context)) { - Log.i(TAG, "Not registered. Skipping."); - return; - } - - try { - boolean needsMultiDeviceSync = performSync(); - - if (TextSecurePreferences.isMultiDevice(context) && needsMultiDeviceSync) { - ApplicationDependencies.getJobManager().add(new MultiDeviceStorageSyncRequestJob()); - } - - SignalStore.storageServiceValues().onSyncCompleted(); - } catch (InvalidKeyException e) { - Log.w(TAG, "Failed to decrypt remote storage! Force-pushing and syncing the storage key to linked devices.", e); - - ApplicationDependencies.getJobManager().startChain(new MultiDeviceKeysUpdateJob()) - .then(new StorageForcePushJob()) - .then(new MultiDeviceStorageSyncRequestJob()) - .enqueue(); - } - } - - @Override - protected boolean onShouldRetry(@NonNull Exception e) { - return e instanceof PushNetworkException || e instanceof RetryLaterException; - } - - @Override - public void onFailure() { - } - - private boolean performSync() throws IOException, RetryLaterException, InvalidKeyException { - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - UnknownStorageIdDatabase storageKeyDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context); - StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey(); - - boolean needsMultiDeviceSync = false; - boolean needsForcePush = false; - long localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context); - Optional remoteManifest = accountManager.getStorageManifestIfDifferentVersion(storageServiceKey, localManifestVersion); - long remoteManifestVersion = remoteManifest.transform(SignalStorageManifest::getVersion).or(localManifestVersion); - - Log.i(TAG, "Our version: " + localManifestVersion + ", their version: " + remoteManifestVersion); - - if (remoteManifest.isPresent() && remoteManifestVersion > localManifestVersion) { - Log.i(TAG, "[Remote Newer] Newer manifest version found!"); - - List allLocalStorageKeys = getAllLocalStorageIds(context, Recipient.self().fresh()); - IdDifferenceResult keyDifference = StorageSyncHelper.findIdDifference(remoteManifest.get().getStorageIds(), allLocalStorageKeys); - - if (keyDifference.hasTypeMismatches()) { - Log.w(TAG, "Found type mismatches in the key sets! Scheduling a force push after this sync completes."); - needsForcePush = true; - } - - if (!keyDifference.isEmpty()) { - Log.i(TAG, "[Remote Newer] There's a difference in keys. Local-only: " + keyDifference.getLocalOnlyIds().size() + ", Remote-only: " + keyDifference.getRemoteOnlyIds().size()); - - List localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyIds()); - List remoteOnly = accountManager.readStorageRecords(storageServiceKey, keyDifference.getRemoteOnlyIds()); - GroupV2ExistenceChecker gv2ExistenceChecker = new StaticGroupV2ExistenceChecker(DatabaseFactory.getGroupDatabase(context).getAllGroupV2Ids()); - MergeResult mergeResult = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, gv2ExistenceChecker); - WriteOperationResult writeOperationResult = StorageSyncHelper.createWriteOperation(remoteManifest.get().getVersion(), allLocalStorageKeys, mergeResult); - - if (remoteOnly.size() != keyDifference.getRemoteOnlyIds().size()) { - Log.w(TAG, "Could not find all remote-only records! Requested: " + keyDifference.getRemoteOnlyIds().size() + ", Found: " + remoteOnly.size() + ". Scheduling a force push after this sync completes."); - needsForcePush = true; - } - - StorageSyncValidations.validate(writeOperationResult, Optional.absent(), needsForcePush, Recipient.self().fresh()); - - Log.i(TAG, "[Remote Newer] MergeResult :: " + mergeResult); - - if (!writeOperationResult.isEmpty()) { - Log.i(TAG, "[Remote Newer] WriteOperationResult :: " + writeOperationResult); - Log.i(TAG, "[Remote Newer] We have something to write remotely."); - - if (writeOperationResult.getManifest().getStorageIds().size() != remoteManifest.get().getStorageIds().size() + writeOperationResult.getInserts().size() - writeOperationResult.getDeletes().size()) { - Log.w(TAG, String.format(Locale.ENGLISH, "Bad storage key management! originalRemoteKeys: %d, newRemoteKeys: %d, insertedKeys: %d, deletedKeys: %d", - remoteManifest.get().getStorageIds().size(), writeOperationResult.getManifest().getStorageIds().size(), writeOperationResult.getInserts().size(), writeOperationResult.getDeletes().size())); - } - - Optional conflict = accountManager.writeStorageRecords(storageServiceKey, writeOperationResult.getManifest(), writeOperationResult.getInserts(), writeOperationResult.getDeletes()); - - if (conflict.isPresent()) { - Log.w(TAG, "[Remote Newer] Hit a conflict when trying to resolve the conflict! Retrying."); - throw new RetryLaterException(); - } - - remoteManifestVersion = writeOperationResult.getManifest().getVersion(); - remoteManifest = Optional.of(writeOperationResult.getManifest()); - - needsMultiDeviceSync = true; - } else { - Log.i(TAG, "[Remote Newer] After resolving the conflict, all changes are local. No remote writes needed."); - } - - migrateToGv2IfNecessary(context, mergeResult.getLocalGroupV2Inserts()); - needsForcePush |= 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()); - - Log.i(TAG, "[Remote Newer] Updating local manifest version to: " + remoteManifestVersion); - TextSecurePreferences.setStorageManifestVersion(context, remoteManifestVersion); - } else { - Log.i(TAG, "[Remote Newer] Remote version was newer, but our local data matched."); - Log.i(TAG, "[Remote Newer] Updating local manifest version to: " + remoteManifest.get().getVersion()); - TextSecurePreferences.setStorageManifestVersion(context, remoteManifest.get().getVersion()); - } - } - - localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context); - - Recipient self = Recipient.self().fresh(); - - List allLocalStorageKeys = getAllLocalStorageIds(context, self); - List pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates(); - List pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions(); - List pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions(); - Optional pendingAccountInsert = StorageSyncHelper.getPendingAccountSyncInsert(context, self); - Optional pendingAccountUpdate = StorageSyncHelper.getPendingAccountSyncUpdate(context, self); - Optional localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion, - allLocalStorageKeys, - pendingUpdates, - pendingInsertions, - pendingDeletions, - pendingAccountUpdate, - pendingAccountInsert); - - if (localWriteResult.isPresent()) { - Log.i(TAG, String.format(Locale.ENGLISH, "[Local Changes] Local changes present. %d updates, %d inserts, %d deletes, account update: %b, account insert: %b.", pendingUpdates.size(), pendingInsertions.size(), pendingDeletions.size(), pendingAccountUpdate.isPresent(), pendingAccountInsert.isPresent())); - - WriteOperationResult localWrite = localWriteResult.get().getWriteResult(); - StorageSyncValidations.validate(localWrite, Optional.absent(), needsForcePush, self); - - Log.i(TAG, "[Local Changes] WriteOperationResult :: " + localWrite); - - if (localWrite.isEmpty()) { - throw new AssertionError("Decided there were local writes, but our write result was empty!"); - } - - Optional conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes()); - - if (conflict.isPresent()) { - Log.w(TAG, "[Local Changes] Hit a conflict when trying to upload our local writes! Retrying."); - throw new RetryLaterException(); - } - - List clearIds = new ArrayList<>(pendingUpdates.size() + pendingInsertions.size() + pendingDeletions.size() + 1); - - clearIds.addAll(Stream.of(pendingUpdates).map(RecipientSettings::getId).toList()); - clearIds.addAll(Stream.of(pendingInsertions).map(RecipientSettings::getId).toList()); - clearIds.addAll(Stream.of(pendingDeletions).map(RecipientSettings::getId).toList()); - clearIds.add(Recipient.self().getId()); - - recipientDatabase.clearDirtyState(clearIds); - recipientDatabase.updateStorageIds(localWriteResult.get().getStorageKeyUpdates()); - - needsMultiDeviceSync = true; - - Log.i(TAG, "[Local Changes] Updating local manifest version to: " + localWriteResult.get().getWriteResult().getManifest().getVersion()); - TextSecurePreferences.setStorageManifestVersion(context, localWriteResult.get().getWriteResult().getManifest().getVersion()); - } else { - Log.i(TAG, "[Local Changes] No local changes."); - } - - if (needsForcePush) { - Log.w(TAG, "Scheduling a force push."); - ApplicationDependencies.getJobManager().add(new StorageForcePushJob()); - } - - return needsMultiDeviceSync; - } - - /** - * Migrates any of the provided V2 IDs that map a local V1 ID. If a match is found, we remove the - * record from the collection of V2 IDs. - */ - private static void migrateToGv2IfNecessary(@NonNull Context context, @NonNull Collection inserts) - throws IOException - { - Map idMap = DatabaseFactory.getGroupDatabase(context).getAllExpectedV2Ids(); - Iterator recordIterator = inserts.iterator(); - - while (recordIterator.hasNext()) { - GroupId.V2 id = GroupId.v2(GroupUtil.requireMasterKey(recordIterator.next().getMasterKeyBytes())); - - if (idMap.containsKey(id)) { - Log.i(TAG, "Discovered a new GV2 ID that is actually a migrated V1 group! Migrating now."); - GroupsV1MigrationUtil.performLocalMigration(context, idMap.get(id)); - recordIterator.remove(); - } - } - } - - private static @NonNull List getAllLocalStorageIds(@NonNull Context context, @NonNull Recipient self) { - return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getContactStorageSyncIds(), - Collections.singletonList(StorageId.forAccount(self.getStorageServiceId())), - DatabaseFactory.getUnknownStorageIdDatabase(context).getAllIds()); - } - - private static @NonNull List buildLocalStorageRecords(@NonNull Context context, @NonNull List ids) { - Recipient self = Recipient.self().fresh(); - RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - UnknownStorageIdDatabase storageKeyDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context); - - List records = new ArrayList<>(ids.size()); - - for (StorageId id : ids) { - switch (id.getType()) { - case ManifestRecord.Identifier.Type.CONTACT_VALUE: - case ManifestRecord.Identifier.Type.GROUPV1_VALUE: - case ManifestRecord.Identifier.Type.GROUPV2_VALUE: - RecipientSettings settings = recipientDatabase.getByStorageId(id.getRaw()); - if (settings != null) { - if (settings.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && settings.getSyncExtras().getGroupMasterKey() == null) { - Log.w(TAG, "Missing master key on gv2 recipient"); - } else { - records.add(StorageSyncModels.localToRemoteRecord(settings)); - } - } else { - Log.w(TAG, "Missing local recipient model! Type: " + id.getType()); - } - break; - case ManifestRecord.Identifier.Type.ACCOUNT_VALUE: - if (!Arrays.equals(self.getStorageServiceId(), id.getRaw())) { - throw new AssertionError("Local storage ID doesn't match self!"); - } - records.add(StorageSyncHelper.buildAccountRecord(context, self)); - break; - default: - SignalStorageRecord unknown = storageKeyDatabase.getById(id.getRaw()); - if (unknown != null) { - records.add(unknown); - } else { - Log.w(TAG, "Missing local unknown model! Type: " + id.getType()); - } - break; - } - } - - return records; - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull StorageSyncJob create(@NonNull Parameters parameters, @NonNull Data data) { - return new StorageSyncJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJobV2.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJobV2.java index 51ec9b22d0..c53faf3a68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJobV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJobV2.java @@ -143,7 +143,7 @@ public class StorageSyncJobV2 extends BaseJob { private static final String TAG = Log.tag(StorageSyncJobV2.class); - StorageSyncJobV2() { + public StorageSyncJobV2() { this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY) .setQueue(QUEUE_KEY) .setMaxInstancesForFactory(2) diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceMigrationJob.java index 316087e60f..1b909bc468 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceMigrationJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/StorageServiceMigrationJob.java @@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; +import org.thoughtcrime.securesms.jobs.StorageSyncJobV2; import org.thoughtcrime.securesms.util.TextSecurePreferences; /** @@ -44,12 +44,12 @@ public class StorageServiceMigrationJob extends MigrationJob { if (TextSecurePreferences.isMultiDevice(context)) { Log.i(TAG, "Multi-device."); - jobManager.startChain(StorageSyncJob.create()) + jobManager.startChain(new StorageSyncJobV2()) .then(new MultiDeviceKeysUpdateJob()) .enqueue(); } else { Log.i(TAG, "Single-device."); - jobManager.add(StorageSyncJob.create()); + jobManager.add(new StorageSyncJobV2()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java index 4778440e8b..c39f83e4bd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreRepository.java @@ -11,7 +11,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.KbsEnclave; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; +import org.thoughtcrime.securesms.jobs.StorageSyncJobV2; import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException; import org.thoughtcrime.securesms.util.Stopwatch; import org.whispersystems.libsignal.util.guava.Optional; @@ -83,7 +83,7 @@ public class PinRestoreRepository { ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN); stopwatch.split("AccountRestore"); - ApplicationDependencies.getJobManager().runSynchronously(StorageSyncJob.create(), TimeUnit.SECONDS.toMillis(10)); + ApplicationDependencies.getJobManager().runSynchronously(new StorageSyncJobV2(), TimeUnit.SECONDS.toMillis(10)); stopwatch.split("ContactRestore"); stopwatch.stop(TAG); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationUtil.java index 839706384f..e4a3e986c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/RegistrationUtil.java @@ -7,7 +7,7 @@ import androidx.annotation.NonNull; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; +import org.thoughtcrime.securesms.jobs.StorageSyncJobV2; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -31,7 +31,7 @@ public final class RegistrationUtil { { Log.i(TAG, "Marking registration completed.", new Throwable()); SignalStore.registrationValues().setRegistrationComplete(); - ApplicationDependencies.getJobManager().startChain(StorageSyncJob.create()) + ApplicationDependencies.getJobManager().startChain(new StorageSyncJobV2()) .then(new DirectoryRefreshJob(false)) .enqueue(); } else if (!SignalStore.registrationValues().isRegistrationComplete()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java deleted file mode 100644 index 33f41d2f60..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java +++ /dev/null @@ -1,143 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.signal.core.util.logging.Log; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.storage.SignalAccountRecord; -import org.whispersystems.signalservice.api.storage.SignalAccountRecord.PinnedConversation; -import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -class AccountConflictMerger implements StorageSyncHelper.ConflictMerger { - - private static final String TAG = Log.tag(AccountConflictMerger.class); - - private final Optional local; - - AccountConflictMerger(Optional local) { - this.local = local; - } - - @Override - public @NonNull Optional getMatching(@NonNull SignalAccountRecord record) { - return local; - } - - @Override - public @NonNull Collection getInvalidEntries(@NonNull Collection remoteRecords) { - Set invalid = new HashSet<>(remoteRecords); - if (remoteRecords.size() > 0) { - invalid.remove(remoteRecords.iterator().next()); - } - - if (invalid.size() > 0) { - Log.w(TAG, "Found invalid account entries! Count: " + invalid.size()); - } - - return invalid; - } - - @Override - public @NonNull SignalAccountRecord merge(@NonNull SignalAccountRecord remote, @NonNull SignalAccountRecord local, @NonNull StorageKeyGenerator keyGenerator) { - String givenName; - String familyName; - - if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) { - givenName = remote.getGivenName().or(""); - familyName = remote.getFamilyName().or(""); - } else { - givenName = local.getGivenName().or(""); - familyName = local.getFamilyName().or(""); - } - - byte[] unknownFields = remote.serializeUnknownFields(); - String avatarUrlPath = remote.getAvatarUrlPath().or(local.getAvatarUrlPath()).or(""); - byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull(); - boolean noteToSelfArchived = remote.isNoteToSelfArchived(); - boolean noteToSelfForcedUnread = remote.isNoteToSelfForcedUnread(); - boolean readReceipts = remote.isReadReceiptsEnabled(); - boolean typingIndicators = remote.isTypingIndicatorsEnabled(); - boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled(); - boolean linkPreviews = remote.isLinkPreviewsEnabled(); - boolean unlisted = remote.isPhoneNumberUnlisted(); - List pinnedConversations = remote.getPinnedConversations(); - AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode(); - boolean preferContactAvatars = remote.isPreferContactAvatars(); - boolean paymentsEnabled = remote.getPayments().isEnabled(); - byte[] paymentsEntropy = remote.getPayments().getEntropy().or(local.getPayments().getEntropy()).orNull(); - boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, paymentsEnabled, paymentsEntropy); - boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, paymentsEnabled, paymentsEntropy); - - if (matchesRemote) { - return remote; - } else if (matchesLocal) { - return local; - } else { - return new SignalAccountRecord.Builder(keyGenerator.generate()) - .setUnknownFields(unknownFields) - .setGivenName(givenName) - .setFamilyName(familyName) - .setAvatarUrlPath(avatarUrlPath) - .setProfileKey(profileKey) - .setNoteToSelfArchived(noteToSelfArchived) - .setNoteToSelfForcedUnread(noteToSelfForcedUnread) - .setReadReceiptsEnabled(readReceipts) - .setTypingIndicatorsEnabled(typingIndicators) - .setSealedSenderIndicatorsEnabled(sealedSenderIndicators) - .setLinkPreviewsEnabled(linkPreviews) - .setUnlistedPhoneNumber(unlisted) - .setPhoneNumberSharingMode(phoneNumberSharingMode) - .setUnlistedPhoneNumber(unlisted) - .setPinnedConversations(pinnedConversations) - .setPreferContactAvatars(preferContactAvatars) - .setPayments(paymentsEnabled, paymentsEntropy) - .build(); - } - } - - private static boolean doParamsMatch(@NonNull SignalAccountRecord contact, - @Nullable byte[] unknownFields, - @NonNull String givenName, - @NonNull String familyName, - @NonNull String avatarUrlPath, - @Nullable byte[] profileKey, - boolean noteToSelfArchived, - boolean noteToSelfForcedUnread, - boolean readReceipts, - boolean typingIndicators, - boolean sealedSenderIndicators, - boolean linkPreviewsEnabled, - AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode, - boolean unlistedPhoneNumber, - @NonNull List pinnedConversations, - boolean preferContactAvatars, - boolean paymentsEnabled, - @Nullable byte[] paymentsEntropy) - { - return Arrays.equals(contact.serializeUnknownFields(), unknownFields) && - Objects.equals(contact.getGivenName().or(""), givenName) && - Objects.equals(contact.getFamilyName().or(""), familyName) && - Objects.equals(contact.getAvatarUrlPath().or(""), avatarUrlPath) && - Arrays.equals(contact.getProfileKey().orNull(), profileKey) && - contact.isNoteToSelfArchived() == noteToSelfArchived && - contact.isNoteToSelfForcedUnread() == noteToSelfForcedUnread && - contact.isReadReceiptsEnabled() == readReceipts && - contact.isTypingIndicatorsEnabled() == typingIndicators && - contact.isSealedSenderIndicatorsEnabled() == sealedSenderIndicators && - contact.isLinkPreviewsEnabled() == linkPreviewsEnabled && - contact.getPhoneNumberSharingMode() == phoneNumberSharingMode && - contact.isPhoneNumberUnlisted() == unlistedPhoneNumber && - contact.isPreferContactAvatars() == preferContactAvatars && - Objects.equals(contact.getPinnedConversations(), pinnedConversations) && - contact.getPayments().isEnabled() == paymentsEnabled && - Arrays.equals(contact.getPayments().getEntropy().orNull(), paymentsEntropy); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java deleted file mode 100644 index a2d19c29fc..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java +++ /dev/null @@ -1,173 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.annimon.stream.Stream; - -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.Base64; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.storage.SignalContactRecord; -import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.UUID; - -class ContactConflictMerger implements StorageSyncHelper.ConflictMerger { - - private static final String TAG = Log.tag(ContactConflictMerger.class); - - private final Map localByUuid = new HashMap<>(); - private final Map localByE164 = new HashMap<>(); - - private final Recipient self; - - ContactConflictMerger(@NonNull Collection localOnly, @NonNull Recipient self) { - for (SignalContactRecord contact : localOnly) { - if (contact.getAddress().getUuid().isPresent()) { - localByUuid.put(contact.getAddress().getUuid().get(), contact); - } - if (contact.getAddress().getNumber().isPresent()) { - localByE164.put(contact.getAddress().getNumber().get(), contact); - } - } - - this.self = self.resolve(); - } - - @Override - public @NonNull Optional getMatching(@NonNull SignalContactRecord record) { - SignalContactRecord localUuid = record.getAddress().getUuid().isPresent() ? localByUuid.get(record.getAddress().getUuid().get()) : null; - SignalContactRecord localE164 = record.getAddress().getNumber().isPresent() ? localByE164.get(record.getAddress().getNumber().get()) : null; - - return Optional.fromNullable(localUuid).or(Optional.fromNullable(localE164)); - } - - @Override - public @NonNull Collection getInvalidEntries(@NonNull Collection remoteRecords) { - Map> localIdToRemoteRecords = new HashMap<>(); - - for (SignalContactRecord remote : remoteRecords) { - Optional local = getMatching(remote); - - if (local.isPresent()) { - String serializedLocalId = Base64.encodeBytes(local.get().getId().getRaw()); - Set matches = localIdToRemoteRecords.get(serializedLocalId); - - if (matches == null) { - matches = new HashSet<>(); - } - - matches.add(remote); - localIdToRemoteRecords.put(serializedLocalId, matches); - } - } - - Set duplicates = new HashSet<>(); - for (Set matches : localIdToRemoteRecords.values()) { - if (matches.size() > 1) { - duplicates.addAll(matches); - } - } - - List selfRecords = Stream.of(remoteRecords) - .filter(r -> r.getAddress().getUuid().equals(self.getUuid()) || r.getAddress().getNumber().equals(self.getE164())) - .toList(); - - Set invalid = new HashSet<>(); - invalid.addAll(selfRecords); - invalid.addAll(duplicates); - - if (invalid.size() > 0) { - Log.w(TAG, "Found invalid contact entries! Self Records: " + selfRecords.size() + ", Duplicates: " + duplicates.size()); - } - - return invalid; - } - - @Override - public @NonNull SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageKeyGenerator keyGenerator) { - String givenName; - String familyName; - - if (remote.getGivenName().isPresent() || remote.getFamilyName().isPresent()) { - givenName = remote.getGivenName().or(""); - familyName = remote.getFamilyName().or(""); - } else { - givenName = local.getGivenName().or(""); - familyName = local.getFamilyName().or(""); - } - - byte[] unknownFields = remote.serializeUnknownFields(); - UUID uuid = remote.getAddress().getUuid().or(local.getAddress().getUuid()).orNull(); - String e164 = remote.getAddress().getNumber().or(local.getAddress().getNumber()).orNull(); - SignalServiceAddress address = new SignalServiceAddress(uuid, e164); - byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull(); - String username = remote.getUsername().or(local.getUsername()).or(""); - IdentityState identityState = remote.getIdentityState(); - byte[] identityKey = remote.getIdentityKey().or(local.getIdentityKey()).orNull(); - boolean blocked = remote.isBlocked(); - boolean profileSharing = remote.isProfileSharingEnabled(); - boolean archived = remote.isArchived(); - boolean forcedUnread = remote.isForcedUnread(); - boolean matchesRemote = doParamsMatch(remote, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread); - boolean matchesLocal = doParamsMatch(local, unknownFields, address, givenName, familyName, profileKey, username, identityState, identityKey, blocked, profileSharing, archived, forcedUnread); - - if (matchesRemote) { - return remote; - } else if (matchesLocal) { - return local; - } else { - return new SignalContactRecord.Builder(keyGenerator.generate(), address) - .setUnknownFields(unknownFields) - .setGivenName(givenName) - .setFamilyName(familyName) - .setProfileKey(profileKey) - .setUsername(username) - .setIdentityState(identityState) - .setIdentityKey(identityKey) - .setBlocked(blocked) - .setProfileSharingEnabled(profileSharing) - .setForcedUnread(forcedUnread) - .build(); - } - } - - private static boolean doParamsMatch(@NonNull SignalContactRecord contact, - @Nullable byte[] unknownFields, - @NonNull SignalServiceAddress address, - @NonNull String givenName, - @NonNull String familyName, - @Nullable byte[] profileKey, - @NonNull String username, - @Nullable IdentityState identityState, - @Nullable byte[] identityKey, - boolean blocked, - boolean profileSharing, - boolean archived, - boolean forcedUnread) - { - return Arrays.equals(contact.serializeUnknownFields(), unknownFields) && - Objects.equals(contact.getAddress(), address) && - Objects.equals(contact.getGivenName().or(""), givenName) && - Objects.equals(contact.getFamilyName().or(""), familyName) && - Arrays.equals(contact.getProfileKey().orNull(), profileKey) && - Objects.equals(contact.getUsername().or(""), username) && - Objects.equals(contact.getIdentityState(), identityState) && - Arrays.equals(contact.getIdentityKey().orNull(), identityKey) && - contact.isBlocked() == blocked && - contact.isProfileSharingEnabled() == profileSharing && - contact.isArchived() == archived && - contact.isForcedUnread() == forcedUnread; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMerger.java deleted file mode 100644 index 71220e5812..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMerger.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; - -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; - -import org.thoughtcrime.securesms.groups.BadGroupIdException; -import org.thoughtcrime.securesms.groups.GroupId; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Map; - -final class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger { - - private final Map localByGroupId; - private final GroupV2ExistenceChecker groupExistenceChecker; - - GroupV1ConflictMerger(@NonNull Collection localOnly, @NonNull GroupV2ExistenceChecker groupExistenceChecker) { - localByGroupId = Stream.of(localOnly).collect(Collectors.toMap(g -> GroupId.v1orThrow(g.getGroupId()), g -> g)); - - this.groupExistenceChecker = groupExistenceChecker; - } - - @Override - public @NonNull Optional getMatching(@NonNull SignalGroupV1Record record) { - return Optional.fromNullable(localByGroupId.get(GroupId.v1orThrow(record.getGroupId()))); - } - - @Override - public @NonNull Collection getInvalidEntries(@NonNull Collection remoteRecords) { - return Stream.of(remoteRecords) - .filter(record -> { - try { - GroupId.V1 id = GroupId.v1(record.getGroupId()); - return groupExistenceChecker.exists(id.deriveV2MigrationGroupId()); - } catch (BadGroupIdException e) { - return true; - } - }).toList(); - } - - @Override - public @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageKeyGenerator keyGenerator) { - byte[] unknownFields = remote.serializeUnknownFields(); - boolean blocked = remote.isBlocked(); - boolean profileSharing = remote.isProfileSharingEnabled(); - boolean archived = remote.isArchived(); - boolean forcedUnread = remote.isForcedUnread(); - - boolean matchesRemote = Arrays.equals(unknownFields, remote.serializeUnknownFields()) && blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived() && forcedUnread == remote.isForcedUnread(); - boolean matchesLocal = Arrays.equals(unknownFields, local.serializeUnknownFields()) && blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived() && forcedUnread == local.isForcedUnread(); - - if (matchesRemote) { - return remote; - } else if (matchesLocal) { - return local; - } else { - return new SignalGroupV1Record.Builder(keyGenerator.generate(), remote.getGroupId()) - .setUnknownFields(unknownFields) - .setBlocked(blocked) - .setProfileSharingEnabled(blocked) - .setForcedUnread(forcedUnread) - .build(); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMerger.java deleted file mode 100644 index 8f3104e9af..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMerger.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; - -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; -import com.google.protobuf.ByteString; - -import org.signal.zkgroup.groups.GroupMasterKey; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Map; - -final class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger { - - private final Map localByMasterKeyBytes; - - GroupV2ConflictMerger(@NonNull Collection localOnly) { - localByMasterKeyBytes = Stream.of(localOnly).collect(Collectors.toMap((SignalGroupV2Record signalGroupV2Record) -> ByteString.copyFrom(signalGroupV2Record.getMasterKeyBytes()), g -> g)); - } - - @Override - public @NonNull Optional getMatching(@NonNull SignalGroupV2Record record) { - return Optional.fromNullable(localByMasterKeyBytes.get(ByteString.copyFrom(record.getMasterKeyBytes()))); - } - - @Override - public @NonNull Collection getInvalidEntries(@NonNull Collection remoteRecords) { - return Stream.of(remoteRecords) - .filterNot(GroupV2ConflictMerger::isValidMasterKey) - .toList(); - } - - @Override - public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageKeyGenerator keyGenerator) { - byte[] unknownFields = remote.serializeUnknownFields(); - boolean blocked = remote.isBlocked(); - boolean profileSharing = remote.isProfileSharingEnabled(); - boolean archived = remote.isArchived(); - boolean forcedUnread = remote.isForcedUnread(); - - boolean matchesRemote = Arrays.equals(unknownFields, remote.serializeUnknownFields()) && blocked == remote.isBlocked() && profileSharing == remote.isProfileSharingEnabled() && archived == remote.isArchived() && forcedUnread == remote.isForcedUnread(); - boolean matchesLocal = Arrays.equals(unknownFields, local.serializeUnknownFields()) && blocked == local.isBlocked() && profileSharing == local.isProfileSharingEnabled() && archived == local.isArchived() && forcedUnread == local.isForcedUnread(); - - if (matchesRemote) { - return remote; - } else if (matchesLocal) { - return local; - } else { - return new SignalGroupV2Record.Builder(keyGenerator.generate(), remote.getMasterKeyBytes()) - .setUnknownFields(unknownFields) - .setBlocked(blocked) - .setProfileSharingEnabled(blocked) - .setArchived(archived) - .setForcedUnread(forcedUnread) - .build(); - } - } - - private static boolean isValidMasterKey(@NonNull SignalGroupV2Record record) { - return record.getMasterKeyBytes().length == GroupMasterKey.SIZE; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ExistenceChecker.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ExistenceChecker.java deleted file mode 100644 index 4271d1a8cb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ExistenceChecker.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.groups.GroupId; - -/** - * Allows a caller to determine if a group exists in the local data store already. Needed primarily - * to check if a local GV2 group already exists for a remote GV1 group. - */ -public interface GroupV2ExistenceChecker { - boolean exists(@NonNull GroupId.V2 groupId); -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StaticGroupV2ExistenceChecker.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StaticGroupV2ExistenceChecker.java deleted file mode 100644 index fe94b07603..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StaticGroupV2ExistenceChecker.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.groups.GroupId; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -/** - * Implementation that is backed by a static set of GV2 IDs. - */ -public final class StaticGroupV2ExistenceChecker implements GroupV2ExistenceChecker { - - private final Set ids; - - public StaticGroupV2ExistenceChecker(@NonNull Collection ids) { - this.ids = new HashSet<>(ids); - } - - @Override - public boolean exists(@NonNull GroupId.V2 groupId) { - return ids.contains(groupId); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java index c45abba499..a02a94e668 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java @@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; +import org.thoughtcrime.securesms.jobs.StorageSyncJobV2; import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.payments.Entropy; @@ -28,9 +28,6 @@ import org.thoughtcrime.securesms.util.Util; 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; import org.whispersystems.signalservice.api.storage.StorageId; @@ -42,7 +39,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -218,114 +214,6 @@ public final class StorageSyncHelper { return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch); } - /** - * Given two sets of storage records, this will resolve the data into a set of actions that need - * to be applied to resolve the differences. This will handle discovering which records between - * the two collections refer to the same contacts and are actually updates, which are brand new, - * etc. - * - * @param remoteOnlyRecords Records that are only present remotely. - * @param localOnlyRecords Records that are only present locally. - * - * @return A set of actions that should be applied to resolve the conflict. - */ - public static @NonNull MergeResult resolveConflict(@NonNull Collection remoteOnlyRecords, - @NonNull Collection localOnlyRecords, - @NonNull GroupV2ExistenceChecker groupExistenceChecker) - { - List remoteOnlyContacts = Stream.of(remoteOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList(); - List localOnlyContacts = Stream.of(localOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList(); - - List remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList(); - List localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList(); - - List remoteOnlyGroupV2 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList(); - List localOnlyGroupV2 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList(); - - List remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList(); - List localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).toList(); - - List remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList(); - List localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList(); - if (remoteOnlyAccount.size() > 0 && localOnlyAccount.isEmpty()) { - throw new AssertionError("Found a remote-only account, but no local-only account!"); - } - if (localOnlyAccount.size() > 1) { - throw new AssertionError("Multiple local accounts?"); - } - - RecordMergeResult contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self())); - RecordMergeResult groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1, groupExistenceChecker)); - RecordMergeResult groupV2MergeResult = resolveRecordConflict(remoteOnlyGroupV2, localOnlyGroupV2, new GroupV2ConflictMerger(localOnlyGroupV2)); - RecordMergeResult accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0)))); - - Set 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> remoteUpdates = new HashSet<>(); - remoteUpdates.addAll(Stream.of(contactMergeResult.remoteUpdates) - .map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forContact(c.getOld()), SignalStorageRecord.forContact(c.getNew()))) - .toList()); - remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates) - .map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew()))) - .toList()); - remoteUpdates.addAll(Stream.of(groupV2MergeResult.remoteUpdates) - .map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forGroupV2(c.getOld()), SignalStorageRecord.forGroupV2(c.getNew()))) - .toList()); - remoteUpdates.addAll(Stream.of(accountMergeResult.remoteUpdates) - .map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew()))) - .toList()); - - Set remoteDeletes = new HashSet<>(); - remoteDeletes.addAll(contactMergeResult.remoteDeletes); - remoteDeletes.addAll(groupV1MergeResult.remoteDeletes); - remoteDeletes.addAll(groupV2MergeResult.remoteDeletes); - remoteDeletes.addAll(accountMergeResult.remoteDeletes); - - return new MergeResult(contactMergeResult.localInserts, - 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()), - remoteInserts, - remoteUpdates, - remoteDeletes); - } - - /** - * Assumes that the merge result has *not* yet been applied to the local data. That means that - * this method will handle generating the correct final key set based on the merge result. - */ - public static @NonNull WriteOperationResult createWriteOperation(long currentManifestVersion, - @NonNull List currentLocalStorageKeys, - @NonNull MergeResult mergeResult) - { - List inserts = new ArrayList<>(); - inserts.addAll(mergeResult.getRemoteInserts()); - inserts.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(StorageRecordUpdate::getNew).toList()); - - List deletes = new ArrayList<>(); - deletes.addAll(Stream.of(mergeResult.getRemoteDeletes()).map(SignalRecord::getId).toList()); - deletes.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(StorageRecordUpdate::getOld).map(SignalStorageRecord::getId).toList()); - - Set completeKeys = new HashSet<>(currentLocalStorageKeys); - completeKeys.addAll(Stream.of(mergeResult.getAllNewRecords()).map(SignalRecord::getId).toList()); - completeKeys.removeAll(Stream.of(mergeResult.getAllRemovedRecords()).map(SignalRecord::getId).toList()); - completeKeys.addAll(Stream.of(inserts).map(SignalStorageRecord::getId).toList()); - completeKeys.removeAll(deletes); - - SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, new ArrayList<>(completeKeys)); - - return new WriteOperationResult(manifest, inserts, Stream.of(deletes).map(StorageId::getRaw).toList()); - } - public static @NonNull byte[] generateKey() { return keyGenerator.generate(); } @@ -335,41 +223,6 @@ public final class StorageSyncHelper { keyGenerator = testKeyGenerator != null ? testKeyGenerator : KEY_GENERATOR; } - private static @NonNull RecordMergeResult resolveRecordConflict(@NonNull Collection remoteOnlyRecords, - @NonNull Collection localOnlyRecords, - @NonNull ConflictMerger merger) - { - Set localInserts = new HashSet<>(remoteOnlyRecords); - Set remoteInserts = new HashSet<>(localOnlyRecords); - Set> localUpdates = new HashSet<>(); - Set> remoteUpdates = new HashSet<>(); - Set remoteDeletes = new HashSet<>(merger.getInvalidEntries(remoteOnlyRecords)); - - remoteOnlyRecords.removeAll(remoteDeletes); - localInserts.removeAll(remoteDeletes); - - for (E remote : remoteOnlyRecords) { - Optional local = merger.getMatching(remote); - - if (local.isPresent()) { - E merged = merger.merge(remote, local.get(), keyGenerator); - - if (!merged.equals(remote)) { - remoteUpdates.add(new StorageRecordUpdate<>(remote, merged)); - } - - if (!merged.equals(local.get())) { - localUpdates.add(new StorageRecordUpdate<>(local.get(), merged)); - } - - localInserts.remove(remote); - remoteInserts.remove(local.get()); - } - } - - return new RecordMergeResult<>(localInserts, localUpdates, remoteInserts, remoteUpdates, remoteDeletes); - } - public static boolean profileKeyChanged(StorageRecordUpdate update) { return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey()); } @@ -417,15 +270,8 @@ public final class StorageSyncHelper { return SignalStorageRecord.forAccount(account); } - public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional> update) { - if (!update.isPresent()) { - return; - } - applyAccountStorageSyncUpdates(context, Recipient.self(), update.get().getNew(), true); - } - public static void applyAccountStorageSyncUpdates(@NonNull Context context, @NonNull Recipient self, @NonNull SignalAccountRecord update, boolean fetchProfile) { - DatabaseFactory.getRecipientDatabase(context).applyStorageSyncUpdates(StorageId.forAccount(self.getStorageServiceId()), update); + DatabaseFactory.getRecipientDatabase(context).applyStorageSyncAccountUpdate(StorageId.forAccount(self.getStorageServiceId()), update); TextSecurePreferences.setReadReceiptsEnabled(context, update.isReadReceiptsEnabled()); TextSecurePreferences.setTypingIndicatorsEnabled(context, update.isTypingIndicatorsEnabled()); @@ -446,7 +292,7 @@ public final class StorageSyncHelper { Log.d(TAG, "Registration still ongoing. Ignore sync request."); return; } - ApplicationDependencies.getJobManager().add(StorageSyncJob.create()); + ApplicationDependencies.getJobManager().add(new StorageSyncJobV2()); } public static void scheduleRoutineSync() { @@ -500,135 +346,6 @@ public final class StorageSyncHelper { } } - public static final class MergeResult { - private final Set localContactInserts; - private final Set> localContactUpdates; - private final Set localGroupV1Inserts; - private final Set> localGroupV1Updates; - private final Set localGroupV2Inserts; - private final Set> localGroupV2Updates; - private final Set localUnknownInserts; - private final Set localUnknownDeletes; - private final Optional> localAccountUpdate; - private final Set remoteInserts; - private final Set> remoteUpdates; - private final Set remoteDeletes; - - @VisibleForTesting - MergeResult(@NonNull Set localContactInserts, - @NonNull Set> localContactUpdates, - @NonNull Set localGroupV1Inserts, - @NonNull Set> localGroupV1Updates, - @NonNull Set localGroupV2Inserts, - @NonNull Set> localGroupV2Updates, - @NonNull Set localUnknownInserts, - @NonNull Set localUnknownDeletes, - @NonNull Optional> localAccountUpdate, - @NonNull Set remoteInserts, - @NonNull Set> remoteUpdates, - @NonNull Set remoteDeletes) - { - this.localContactInserts = localContactInserts; - this.localContactUpdates = localContactUpdates; - this.localGroupV1Inserts = localGroupV1Inserts; - this.localGroupV1Updates = localGroupV1Updates; - this.localGroupV2Inserts = localGroupV2Inserts; - this.localGroupV2Updates = localGroupV2Updates; - this.localUnknownInserts = localUnknownInserts; - this.localUnknownDeletes = localUnknownDeletes; - this.localAccountUpdate = localAccountUpdate; - this.remoteInserts = remoteInserts; - this.remoteUpdates = remoteUpdates; - this.remoteDeletes = remoteDeletes; - } - - public @NonNull Set getLocalContactInserts() { - return localContactInserts; - } - - public @NonNull Set> getLocalContactUpdates() { - return localContactUpdates; - } - - public @NonNull Set getLocalGroupV1Inserts() { - return localGroupV1Inserts; - } - - public @NonNull Set> getLocalGroupV1Updates() { - return localGroupV1Updates; - } - - public @NonNull Set getLocalGroupV2Inserts() { - return localGroupV2Inserts; - } - - public @NonNull Set> getLocalGroupV2Updates() { - return localGroupV2Updates; - } - - public @NonNull Set getLocalUnknownInserts() { - return localUnknownInserts; - } - - public @NonNull Set getLocalUnknownDeletes() { - return localUnknownDeletes; - } - - public @NonNull Optional> getLocalAccountUpdate() { - return localAccountUpdate; - } - - public @NonNull Set getRemoteInserts() { - return remoteInserts; - } - - public @NonNull Set> getRemoteUpdates() { - return remoteUpdates; - } - - public @NonNull Set getRemoteDeletes() { - return remoteDeletes; - } - - @NonNull Set getAllNewRecords() { - Set records = new HashSet<>(); - - records.addAll(localContactInserts); - records.addAll(localGroupV1Inserts); - records.addAll(localGroupV2Inserts); - records.addAll(remoteInserts); - records.addAll(localUnknownInserts); - records.addAll(Stream.of(localContactUpdates).map(StorageRecordUpdate::getNew).toList()); - records.addAll(Stream.of(localGroupV1Updates).map(StorageRecordUpdate::getNew).toList()); - records.addAll(Stream.of(localGroupV2Updates).map(StorageRecordUpdate::getNew).toList()); - records.addAll(Stream.of(remoteUpdates).map(StorageRecordUpdate::getNew).toList()); - if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getNew()); - - return records; - } - - @NonNull Set getAllRemovedRecords() { - Set records = new HashSet<>(); - - records.addAll(localUnknownDeletes); - records.addAll(Stream.of(localContactUpdates).map(StorageRecordUpdate::getOld).toList()); - records.addAll(Stream.of(localGroupV1Updates).map(StorageRecordUpdate::getOld).toList()); - records.addAll(Stream.of(localGroupV2Updates).map(StorageRecordUpdate::getOld).toList()); - records.addAll(Stream.of(remoteUpdates).map(StorageRecordUpdate::getOld).toList()); - records.addAll(remoteDeletes); - if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getOld()); - - return records; - } - - @Override - public @NonNull String toString() { - return String.format(Locale.ENGLISH, - "localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localGroupV2Inserts: %d, localGroupV2Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d, remoteDeletes: %d", - localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localGroupV2Inserts.size(), localGroupV2Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size(), remoteDeletes.size()); - } - } - public static final class WriteOperationResult { private final SignalStorageManifest manifest; private final List inserts; @@ -692,33 +409,6 @@ public final class StorageSyncHelper { } } - private static class RecordMergeResult { - final Set localInserts; - final Set> localUpdates; - final Set remoteInserts; - final Set> remoteUpdates; - final Set remoteDeletes; - - RecordMergeResult(@NonNull Set localInserts, - @NonNull Set> localUpdates, - @NonNull Set remoteInserts, - @NonNull Set> remoteUpdates, - @NonNull Set remoteDeletes) - { - this.localInserts = localInserts; - this.localUpdates = localUpdates; - this.remoteInserts = remoteInserts; - this.remoteUpdates = remoteUpdates; - this.remoteDeletes = remoteDeletes; - } - } - - interface ConflictMerger { - @NonNull Optional getMatching(@NonNull E record); - @NonNull Collection getInvalidEntries(@NonNull Collection remoteRecords); - @NonNull E merge(@NonNull E remote, @NonNull E local, @NonNull StorageKeyGenerator keyGenerator); - } - private static final class MultipleExistingAccountsException extends IllegalArgumentException {} private static final class InvalidAccountInsertException extends IllegalArgumentException {} private static final class InvalidAccountUpdateException extends IllegalArgumentException {} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java index ab79e74f87..e702f7191a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FeatureFlags.java @@ -75,7 +75,6 @@ public final class FeatureFlags { private static final String ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory"; private static final String MESSAGE_PROCESSOR_ALARM_INTERVAL = "android.messageProcessor.alarmIntervalMins"; private static final String MESSAGE_PROCESSOR_DELAY = "android.messageProcessor.foregroundDelayMs"; - private static final String STORAGE_SYNC_V2 = "android.storageSyncV2.3"; private static final String NOTIFICATION_REWRITE = "android.notificationRewrite"; private static final String MP4_GIF_SEND_SUPPORT = "android.mp4GifSendSupport"; @@ -109,7 +108,6 @@ public final class FeatureFlags { ANIMATED_STICKER_MIN_TOTAL_MEMORY, MESSAGE_PROCESSOR_ALARM_INTERVAL, MESSAGE_PROCESSOR_DELAY, - STORAGE_SYNC_V2, NOTIFICATION_REWRITE, MP4_GIF_SEND_SUPPORT ); @@ -155,7 +153,6 @@ public final class FeatureFlags { MESSAGE_PROCESSOR_ALARM_INTERVAL, MESSAGE_PROCESSOR_DELAY, GV1_FORCED_MIGRATE, - STORAGE_SYNC_V2, NOTIFICATION_REWRITE, MP4_GIF_SEND_SUPPORT ); @@ -344,11 +341,6 @@ public final class FeatureFlags { return getInteger(ANIMATED_STICKER_MIN_TOTAL_MEMORY, (int) ByteUnit.GIGABYTES.toMegabytes(3)); } - /** Whether or not to use {@link org.thoughtcrime.securesms.jobs.StorageSyncJobV2}. */ - public static boolean storageSyncV2() { - return getBoolean(STORAGE_SYNC_V2, true); - } - /** Whether or not to use the new notification system. */ public static boolean useNewNotificationSystem() { return getBoolean(NOTIFICATION_REWRITE, false) && Build.VERSION.SDK_INT >= 26; diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java deleted file mode 100644 index 453a99c892..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java +++ /dev/null @@ -1,185 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import org.junit.Test; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.storage.SignalContactRecord; -import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; - -import java.util.Collection; -import java.util.Collections; -import java.util.UUID; - -import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.mockito.Mockito.when; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.thoughtcrime.securesms.testutil.TestHelpers.assertContentsEqual; -import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray; -import static org.thoughtcrime.securesms.testutil.TestHelpers.setOf; - -public class ContactConflictMergerTest { - - private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7"); - private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1"); - private static final UUID UUID_SELF = UuidUtil.parseOrThrow("1b2a2ca5-fc9e-4656-8c9f-22cc349ed3af"); - - - private static final String E164_A = "+16108675309"; - private static final String E164_B = "+16101234567"; - private static final String E164_SELF = "+16105555555"; - - private static final Recipient SELF = mock(Recipient.class); - static { - when(SELF.getUuid()).thenReturn(Optional.of(UUID_SELF)); - when(SELF.getE164()).thenReturn(Optional.of(E164_SELF)); - when(SELF.resolve()).thenReturn(SELF); - Log.initialize(new Log.Logger[0]); - } - - @Test - public void merge_alwaysPreferRemote() { - SignalContactRecord remote = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)) - .setBlocked(true) - .setIdentityKey(byteArray(2)) - .setIdentityState(IdentityState.VERIFIED) - .setProfileKey(byteArray(3)) - .setGivenName("AFirst") - .setFamilyName("ALast") - .setUsername("username A") - .setProfileSharingEnabled(false) - .setArchived(false) - .setForcedUnread(false) - .build(); - SignalContactRecord local = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)) - .setBlocked(false) - .setIdentityKey(byteArray(99)) - .setIdentityState(IdentityState.DEFAULT) - .setProfileKey(byteArray(999)) - .setGivenName("BFirst") - .setFamilyName("BLast") - .setUsername("username B") - .setProfileSharingEnabled(true) - .setArchived(true) - .setForcedUnread(true) - .build(); - - SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(StorageKeyGenerator.class)); - - assertEquals(UUID_A, merged.getAddress().getUuid().get()); - assertEquals(E164_A, merged.getAddress().getNumber().get()); - assertTrue(merged.isBlocked()); - assertArrayEquals(byteArray(2), merged.getIdentityKey().get()); - assertEquals(IdentityState.VERIFIED, merged.getIdentityState()); - assertArrayEquals(byteArray(3), merged.getProfileKey().get()); - assertEquals("AFirst", merged.getGivenName().get()); - assertEquals("ALast", merged.getFamilyName().get()); - assertEquals("username A", merged.getUsername().get()); - assertFalse(merged.isProfileSharingEnabled()); - assertFalse(merged.isArchived()); - assertFalse(merged.isForcedUnread()); - } - - @Test - public void merge_fillInGaps_treatNamePartsAsOneUnit() { - SignalContactRecord remote = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, null)) - .setBlocked(true) - .setGivenName("AFirst") - .setFamilyName("") - .setProfileSharingEnabled(true) - .build(); - SignalContactRecord local = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)) - .setBlocked(false) - .setIdentityKey(byteArray(2)) - .setProfileKey(byteArray(3)) - .setGivenName("BFirst") - .setFamilyName("BLast") - .setUsername("username B") - .setProfileSharingEnabled(false) - .build(); - SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(StorageKeyGenerator.class)); - - assertEquals(UUID_A, merged.getAddress().getUuid().get()); - assertEquals(E164_B, merged.getAddress().getNumber().get()); - assertTrue(merged.isBlocked()); - assertArrayEquals(byteArray(2), merged.getIdentityKey().get()); - assertEquals(IdentityState.DEFAULT, merged.getIdentityState()); - assertArrayEquals(byteArray(3), merged.getProfileKey().get()); - assertEquals("AFirst", merged.getGivenName().get()); - assertFalse(merged.getFamilyName().isPresent()); - assertEquals("username B", merged.getUsername().get()); - assertTrue(merged.isProfileSharingEnabled()); - } - - @Test - public void merge_returnRemoteIfEndResultMatchesRemote() { - SignalContactRecord remote = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)) - .setBlocked(true) - .setGivenName("AFirst") - .setFamilyName("") - .setUsername("username B") - .setProfileKey(byteArray(3)) - .setProfileSharingEnabled(true) - .build(); - SignalContactRecord local = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(null, E164_A)) - .setBlocked(false) - .setGivenName("BFirst") - .setFamilyName("BLast") - .setProfileSharingEnabled(false) - .build(); - SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(StorageKeyGenerator.class)); - - assertEquals(remote, merged); - } - - @Test - public void merge_returnLocalIfEndResultMatchesLocal() { - SignalContactRecord remote = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build(); - SignalContactRecord local = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_A, E164_A)) - .setGivenName("AFirst") - .setFamilyName("ALast") - .build(); - SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(StorageKeyGenerator.class)); - - assertEquals(local, merged); - } - - @Test - public void getInvalidEntries_nothingInvalid() { - SignalContactRecord a = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build(); - SignalContactRecord b = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)).build(); - - Collection invalid = new ContactConflictMerger(Collections.emptyList(), SELF).getInvalidEntries(setOf(a, b)); - - assertContentsEqual(setOf(), invalid); - } - - @Test - public void getInvalidEntries_selfIsInvalid() { - SignalContactRecord a = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build(); - SignalContactRecord b = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)).build(); - SignalContactRecord self = new SignalContactRecord.Builder(byteArray(3), new SignalServiceAddress(UUID_SELF, E164_SELF)).build(); - - Collection invalid = new ContactConflictMerger(Collections.emptyList(), SELF).getInvalidEntries(setOf(a, b, self)); - - assertContentsEqual(setOf(self), invalid); - } - - @Test - public void getInvalidEntries_duplicatesInvalid() { - SignalContactRecord aLocal = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build(); - SignalContactRecord bRemote = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)).build(); - SignalContactRecord aRemote1 = new SignalContactRecord.Builder(byteArray(3), new SignalServiceAddress(UUID_A, null)).build(); - SignalContactRecord aRemote2 = new SignalContactRecord.Builder(byteArray(4), new SignalServiceAddress(null, E164_A)).build(); - SignalContactRecord aRemote3 = new SignalContactRecord.Builder(byteArray(5), new SignalServiceAddress(UUID_A, E164_A)).build(); - - Collection invalid = new ContactConflictMerger(Collections.singleton(aLocal), SELF).getInvalidEntries(setOf(aRemote1, aRemote2, aRemote3, bRemote)); - - assertContentsEqual(setOf(aRemote1, aRemote2, aRemote3), invalid); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMergerTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMergerTest.java deleted file mode 100644 index f4ebdb9c54..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMergerTest.java +++ /dev/null @@ -1,123 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import org.junit.Test; -import org.thoughtcrime.securesms.groups.GroupId; -import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.powermock.api.mockito.PowerMockito.when; -import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray; -import static org.thoughtcrime.securesms.testutil.ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS; - -public final class GroupV1ConflictMergerTest { - - private static final byte[] GENERATED_KEY = byteArray(8675309); - private static final StorageKeyGenerator KEY_GENERATOR = mock(StorageKeyGenerator.class); - - static { - when(KEY_GENERATOR.generate()).thenReturn(GENERATED_KEY); - } - - @Test - public void merge_alwaysPreferRemote() { - SignalGroupV1Record remote = new SignalGroupV1Record.Builder(byteArray(1), byteArray(100, 16)) - .setBlocked(false) - .setProfileSharingEnabled(false) - .setArchived(false) - .setForcedUnread(false) - .build(); - SignalGroupV1Record local = new SignalGroupV1Record.Builder(byteArray(2), byteArray(100, 16)) - .setBlocked(true) - .setProfileSharingEnabled(true) - .setArchived(true) - .setForcedUnread(true) - .build(); - - SignalGroupV1Record merged = new GroupV1ConflictMerger(Collections.singletonList(local), id -> false).merge(remote, local, KEY_GENERATOR); - - assertArrayEquals(remote.getId().getRaw(), merged.getId().getRaw()); - assertArrayEquals(byteArray(100, 16), merged.getGroupId()); - assertFalse(merged.isProfileSharingEnabled()); - assertFalse(merged.isBlocked()); - assertFalse(merged.isArchived()); - assertFalse(merged.isForcedUnread()); - } - - @Test - public void merge_returnRemoteIfEndResultMatchesRemote() { - SignalGroupV1Record remote = new SignalGroupV1Record.Builder(byteArray(1), byteArray(100, 16)) - .setBlocked(false) - .setProfileSharingEnabled(true) - .setArchived(true) - .build(); - SignalGroupV1Record local = new SignalGroupV1Record.Builder(byteArray(2), byteArray(100, 16)) - .setBlocked(true) - .setProfileSharingEnabled(false) - .setArchived(false) - .build(); - - SignalGroupV1Record merged = new GroupV1ConflictMerger(Collections.singletonList(local), id -> false).merge(remote, local, mock(StorageKeyGenerator.class)); - - assertEquals(remote, merged); - } - - @Test - public void merge_excludeBadGroupId() { - assumeZkGroupSupportedOnOS(); - - SignalGroupV1Record badRemote = new SignalGroupV1Record.Builder(byteArray(1), badGroupKey(99)) - .setBlocked(false) - .setProfileSharingEnabled(true) - .setArchived(true) - .build(); - - SignalGroupV1Record goodRemote = new SignalGroupV1Record.Builder(byteArray(1), groupKey(99)) - .setBlocked(false) - .setProfileSharingEnabled(true) - .setArchived(true) - .build(); - - Collection invalid = new GroupV1ConflictMerger(Collections.emptyList(), id -> false).getInvalidEntries(Arrays.asList(badRemote, goodRemote)); - - assertEquals(Collections.singletonList(badRemote), invalid); - } - - @Test - public void merge_excludeMigratedGroupId() { - assumeZkGroupSupportedOnOS(); - - GroupId.V1 v1Id = GroupId.v1orThrow(groupKey(1)); - GroupId.V2 v2Id = v1Id.deriveV2MigrationGroupId(); - - SignalGroupV1Record badRemote = new SignalGroupV1Record.Builder(byteArray(1), v1Id.getDecodedId()) - .setBlocked(false) - .setProfileSharingEnabled(true) - .setArchived(true) - .build(); - - SignalGroupV1Record goodRemote = new SignalGroupV1Record.Builder(byteArray(1), groupKey(99)) - .setBlocked(false) - .setProfileSharingEnabled(true) - .setArchived(true) - .build(); - - Collection invalid = new GroupV1ConflictMerger(Collections.emptyList(), id -> id.equals(v2Id)).getInvalidEntries(Arrays.asList(badRemote, goodRemote)); - - assertEquals(Collections.singletonList(badRemote), invalid); - } - - private static byte[] groupKey(int value) { - return byteArray(value, 16); - } - - private static byte[] badGroupKey(int value) { - return byteArray(value, 32); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMergerTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMergerTest.java deleted file mode 100644 index 98fe335756..0000000000 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMergerTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.thoughtcrime.securesms.storage; - -import org.junit.Test; -import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.powermock.api.mockito.PowerMockito.mock; -import static org.powermock.api.mockito.PowerMockito.when; -import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray; - -public final class GroupV2ConflictMergerTest { - - private static final byte[] GENERATED_KEY = byteArray(8675309); - private static final StorageKeyGenerator KEY_GENERATOR = mock(StorageKeyGenerator.class); - - static { - when(KEY_GENERATOR.generate()).thenReturn(GENERATED_KEY); - } - - @Test - public void merge_alwaysPreferRemote() { - SignalGroupV2Record remote = new SignalGroupV2Record.Builder(byteArray(1), groupKey(100)) - .setBlocked(false) - .setProfileSharingEnabled(false) - .setArchived(false) - .setForcedUnread(false) - .build(); - SignalGroupV2Record local = new SignalGroupV2Record.Builder(byteArray(2), groupKey(100)) - .setBlocked(true) - .setProfileSharingEnabled(true) - .setArchived(true) - .setForcedUnread(true) - .build(); - - SignalGroupV2Record merged = new GroupV2ConflictMerger(Collections.singletonList(local)).merge(remote, local, KEY_GENERATOR); - - assertArrayEquals(remote.getId().getRaw(), merged.getId().getRaw()); - assertArrayEquals(groupKey(100), merged.getMasterKeyBytes()); - assertFalse(merged.isProfileSharingEnabled()); - assertFalse(merged.isBlocked()); - assertFalse(merged.isArchived()); - assertFalse(merged.isForcedUnread()); - } - - @Test - public void merge_returnRemoteIfEndResultMatchesRemote() { - SignalGroupV2Record remote = new SignalGroupV2Record.Builder(byteArray(1), groupKey(100)) - .setBlocked(false) - .setProfileSharingEnabled(true) - .setArchived(true) - .build(); - SignalGroupV2Record local = new SignalGroupV2Record.Builder(byteArray(2), groupKey(100)) - .setBlocked(true) - .setProfileSharingEnabled(false) - .setArchived(false) - .build(); - - SignalGroupV2Record merged = new GroupV2ConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(StorageKeyGenerator.class)); - - assertEquals(remote, merged); - } - - @Test - public void merge_excludeBadGroupId() { - SignalGroupV2Record badRemote = new SignalGroupV2Record.Builder(byteArray(1), badGroupKey(99)) - .setBlocked(false) - .setProfileSharingEnabled(true) - .setArchived(true) - .build(); - - SignalGroupV2Record goodRemote = new SignalGroupV2Record.Builder(byteArray(1), groupKey(99)) - .setBlocked(false) - .setProfileSharingEnabled(true) - .setArchived(true) - .build(); - - Collection invalid = new GroupV2ConflictMerger(Collections.emptyList()).getInvalidEntries(Arrays.asList(badRemote, goodRemote)); - - assertEquals(Collections.singletonList(badRemote), invalid); - } - - private static byte[] groupKey(int value) { - return byteArray(value, 32); - } - - private static byte[] badGroupKey(int value) { - return byteArray(value, 16); - } -} diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java index ff5a207d0b..963592f17c 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java @@ -15,9 +15,7 @@ import org.powermock.modules.junit4.PowerMockRunnerDelegate; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult; -import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult; import org.thoughtcrime.securesms.util.FeatureFlags; -import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; @@ -31,7 +29,6 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -39,19 +36,15 @@ import java.util.Set; import java.util.UUID; import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; import static org.powermock.api.mockito.PowerMockito.mockStatic; -import static org.thoughtcrime.securesms.testutil.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS; -import static org.thoughtcrime.securesms.testutil.TestHelpers.assertByteListEquals; import static org.thoughtcrime.securesms.testutil.TestHelpers.assertContentsEqual; import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray; import static org.thoughtcrime.securesms.testutil.TestHelpers.byteListOf; -import static org.thoughtcrime.securesms.testutil.TestHelpers.setOf; @RunWith(PowerMockRunner.class) @PrepareForTest({ Recipient.class, FeatureFlags.class}) @@ -147,231 +140,6 @@ public final class StorageSyncHelperTest { assertTrue(result.hasTypeMismatches()); } - @Test - public void resolveConflict_noOverlap() { - SignalContactRecord remote1 = contact(1, UUID_A, E164_A, "a"); - SignalContactRecord local1 = contact(2, UUID_B, E164_B, "b"); - - MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1), r -> false); - - assertEquals(setOf(remote1), result.getLocalContactInserts()); - assertTrue(result.getLocalContactUpdates().isEmpty()); - assertEquals(setOf(SignalStorageRecord.forContact(local1)), result.getRemoteInserts()); - assertTrue(result.getRemoteUpdates().isEmpty()); - assertTrue(result.getRemoteDeletes().isEmpty()); - } - - @Test - public void resolveConflict_contact_deleteSelfContact() { - SignalContactRecord remote1 = contact(1, UUID_SELF, E164_SELF, "self"); - SignalContactRecord local1 = contact(2, UUID_A, E164_A, "a"); - - MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1), r -> false); - - assertTrue(result.getLocalContactInserts().isEmpty()); - assertTrue(result.getLocalContactUpdates().isEmpty()); - assertEquals(setOf(record(local1)), result.getRemoteInserts()); - assertTrue(result.getRemoteUpdates().isEmpty()); - assertEquals(setOf(remote1), result.getRemoteDeletes()); - } - - @Test - public void resolveConflict_contact_deleteBadGv1() { - SignalGroupV1Record remote1 = badGroupV1(1, 1, true, false); - SignalGroupV1Record local1 = groupV1(2, 1, true, true); - - MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1), r -> false); - - assertTrue(result.getLocalContactInserts().isEmpty()); - assertTrue(result.getLocalContactUpdates().isEmpty()); - assertEquals(setOf(record(local1)), result.getRemoteInserts()); - assertTrue(result.getRemoteUpdates().isEmpty()); - assertEquals(setOf(remote1), result.getRemoteDeletes()); - } - - @Test - public void resolveConflict_contact_deleteBadGv2() { - SignalGroupV2Record remote1 = badGroupV2(1, 2, true, false); - SignalGroupV2Record local1 = groupV2(2, 2, true, false); - - MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1), r -> false); - - assertTrue(result.getLocalContactInserts().isEmpty()); - assertTrue(result.getLocalContactUpdates().isEmpty()); - assertEquals(setOf(record(local1)), result.getRemoteInserts()); - assertTrue(result.getRemoteUpdates().isEmpty()); - assertEquals(setOf(remote1), result.getRemoteDeletes()); - } - - @Test - public void resolveConflict_contact_sameAsRemote() { - SignalContactRecord remote1 = contact(1, UUID_A, E164_A, "a"); - SignalContactRecord local1 = contact(2, UUID_A, E164_A, "a"); - - MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1), r -> false); - - SignalContactRecord expectedMerge = contact(1, UUID_A, E164_A, "a"); - - assertTrue(result.getLocalContactInserts().isEmpty()); - assertEquals(setOf(update(local1, expectedMerge)), result.getLocalContactUpdates()); - assertTrue(result.getRemoteInserts().isEmpty()); - assertTrue(result.getRemoteUpdates().isEmpty()); - assertTrue(result.getRemoteDeletes().isEmpty()); - } - - @Test - public void resolveConflict_group_v1_sameAsRemote() { - assumeLibSignalSupportedOnOS(); - - SignalGroupV1Record remote1 = groupV1(1, 1, true, false); - SignalGroupV1Record local1 = groupV1(2, 1, true, false); - - MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1), r -> false); - - SignalGroupV1Record expectedMerge = groupV1(1, 1, true, false); - - assertTrue(result.getLocalContactInserts().isEmpty()); - assertEquals(setOf(update(local1, expectedMerge)), result.getLocalGroupV1Updates()); - assertTrue(result.getRemoteInserts().isEmpty()); - 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), r -> false); - - 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() { - SignalContactRecord remote1 = contact(1, UUID_A, E164_A, null); - SignalContactRecord local1 = contact(2, UUID_A, E164_A, "a"); - - MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1), r -> false); - - SignalContactRecord expectedMerge = contact(2, UUID_A, E164_A, "a"); - - assertTrue(result.getLocalContactInserts().isEmpty()); - assertTrue(result.getLocalContactUpdates().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 local1 = unknown(1); - SignalStorageRecord local2 = unknown(2); - - MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, account), setOf(local1, local2, account), r -> false); - - assertTrue(result.getLocalContactInserts().isEmpty()); - assertTrue(result.getLocalContactUpdates().isEmpty()); - assertEquals(setOf(remote1, remote2), result.getLocalUnknownInserts()); - assertEquals(setOf(local1, local2), result.getLocalUnknownDeletes()); - assertTrue(result.getRemoteDeletes().isEmpty()); - } - - @Test - public void resolveConflict_complex() { - assumeLibSignalSupportedOnOS(); - - SignalContactRecord remote1 = contact(1, UUID_A, null, "a"); - SignalContactRecord local1 = contact(2, UUID_A, E164_A, "a"); - - SignalContactRecord remote2 = contact(3, UUID_B, E164_B, null); - SignalContactRecord local2 = contact(4, UUID_B, null, "b"); - - SignalContactRecord remote3 = contact(5, UUID_C, E164_C, "c"); - SignalContactRecord local3 = contact(6, UUID_D, E164_D, "d"); - - SignalGroupV1Record remote4 = groupV1(7, 1, true, false); - SignalGroupV1Record local4 = groupV1(8, 1, false, true); - - SignalGroupV2Record remote5 = groupV2(9, 2, true, false); - SignalGroupV2Record local5 = groupV2(10, 2, false, true); - - SignalAccountRecord remote6 = account(11); - SignalAccountRecord local6 = account(12); - - SignalStorageRecord unknownRemote = unknown(13); - SignalStorageRecord unknownLocal = unknown(14); - - StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111)); - - Set remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, remote6, unknownRemote); - Set localOnly = recordSetOf(local1, local2, local3, local4, local5, local6, unknownLocal); - - MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false); - - SignalContactRecord merge1 = contact(2, UUID_A, E164_A, "a"); - SignalContactRecord merge2 = contact(111, UUID_B, E164_B, "b"); - - assertEquals(setOf(remote3), result.getLocalContactInserts()); - assertEquals(setOf(update(local2, merge2)), result.getLocalContactUpdates()); - assertEquals(setOf(update(local4, remote4)), result.getLocalGroupV1Updates()); - assertEquals(setOf(update(local5, remote5)), result.getLocalGroupV2Updates()); - assertEquals(setOf(SignalStorageRecord.forContact(local3)), result.getRemoteInserts()); - assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2)), result.getRemoteUpdates()); - assertEquals(Optional.of(update(local6, remote6)), result.getLocalAccountUpdate()); - assertEquals(setOf(unknownRemote), result.getLocalUnknownInserts()); - assertEquals(setOf(unknownLocal), result.getLocalUnknownDeletes()); - assertTrue(result.getRemoteDeletes().isEmpty()); - } - - @Test - public void createWriteOperation_generic() { - List 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"); - SignalContactRecord insert2 = contact(7, UUID_C, E164_C, "c"); - SignalContactRecord old2 = contact(2, UUID_D, E164_D, "d"); - SignalContactRecord new2 = contact(8, UUID_D, E164_D, "z2"); - 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); - - StorageSyncHelper.WriteOperationResult result = StorageSyncHelper.createWriteOperation(1, - localKeys, - new MergeResult(setOf(insert2), - 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, 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), 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 public void ContactUpdate_equals_sameProfileKeys() { byte[] profileKey = new byte[32]; @@ -401,83 +169,6 @@ public final class StorageSyncHelperTest { assertTrue(StorageSyncHelper.profileKeyChanged(update(a, b))); } - @Test - public void resolveConflict_payments_enabled_remotely() { - SignalAccountRecord remoteAccount = accountWithPayments(1, true, new byte[32]); - SignalAccountRecord localAccount = accountWithPayments(2, false, new byte[32]); - - Set remoteOnly = recordSetOf(remoteAccount); - Set localOnly = recordSetOf(localAccount); - - MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false); - - assertTrue(result.getLocalAccountUpdate().get().getNew().getPayments().isEnabled()); - } - - @Test - public void resolveConflict_payments_disabled_remotely() { - SignalAccountRecord remoteAccount = accountWithPayments(1, false, new byte[32]); - SignalAccountRecord localAccount = accountWithPayments(2, true, new byte[32]); - - Set remoteOnly = recordSetOf(remoteAccount); - Set localOnly = recordSetOf(localAccount); - - MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false); - - assertFalse(result.getLocalAccountUpdate().get().getNew().getPayments().isEnabled()); - } - - @Test - public void resolveConflict_payments_remote_entropy_overrides_local_if_correct_length_32() { - byte[] remoteEntropy = Util.getSecretBytes(32); - byte[] localEntropy = Util.getSecretBytes(32); - SignalAccountRecord remoteAccount = accountWithPayments(1, true, remoteEntropy); - SignalAccountRecord localAccount = accountWithPayments(2, true, localEntropy); - - Set remoteOnly = recordSetOf(remoteAccount); - Set localOnly = recordSetOf(localAccount); - - MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false); - - SignalAccountRecord.Payments payments = result.getLocalAccountUpdate().get().getNew().getPayments(); - assertTrue(payments.isEnabled()); - assertArrayEquals(remoteEntropy, payments.getEntropy().get()); - } - - @Test - public void resolveConflict_payments_local_entropy_preserved_if_remote_empty() { - byte[] remoteEntropy = new byte[0]; - byte[] localEntropy = Util.getSecretBytes(32); - SignalAccountRecord remoteAccount = accountWithPayments(1, true, remoteEntropy); - SignalAccountRecord localAccount = accountWithPayments(2, true, localEntropy); - - Set remoteOnly = recordSetOf(remoteAccount); - Set localOnly = recordSetOf(localAccount); - - MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false); - - SignalAccountRecord.Payments payments = result.getLocalAccountUpdate().get().getNew().getPayments(); - assertFalse(payments.isEnabled()); - assertArrayEquals(localEntropy, payments.getEntropy().get()); - } - - @Test - public void resolveConflict_payments_local_entropy_preserved_if_remote_is_a_bad_length() { - byte[] remoteEntropy = Util.getSecretBytes(30); - byte[] localEntropy = Util.getSecretBytes(32); - SignalAccountRecord remoteAccount = accountWithPayments(1, true, remoteEntropy); - SignalAccountRecord localAccount = accountWithPayments(2, true, localEntropy); - - Set remoteOnly = recordSetOf(remoteAccount); - Set localOnly = recordSetOf(localAccount); - - MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, r -> false); - - SignalAccountRecord.Payments payments = result.getLocalAccountUpdate().get().getNew().getPayments(); - assertFalse(payments.isEnabled()); - assertArrayEquals(localEntropy, payments.getEntropy().get()); - } - private static Set recordSetOf(SignalRecord... records) { LinkedHashSet storageRecords = new LinkedHashSet<>(); @@ -502,16 +193,6 @@ public final class StorageSyncHelperTest { } } - private static Set recordSetOf(SignalGroupV1Record... groupRecords) { - LinkedHashSet storageRecords = new LinkedHashSet<>(); - - for (SignalGroupV1Record contactRecord : groupRecords) { - storageRecords.add(SignalStorageRecord.forGroupV1(contactRecord.getId(), contactRecord)); - } - - return storageRecords; - } - private static SignalContactRecord.Builder contactBuilder(int key, UUID uuid, String e164, @@ -521,14 +202,6 @@ public final class StorageSyncHelperTest { .setGivenName(profileName); } - private static SignalAccountRecord account(int key) { - return new SignalAccountRecord.Builder(byteArray(key)).build(); - } - - private static SignalAccountRecord accountWithPayments(int key, boolean enabled, byte[] entropy) { - return new SignalAccountRecord.Builder(byteArray(key)).setPayments(enabled, entropy).build(); - } - private static SignalContactRecord contact(int key, UUID uuid, String e164, @@ -545,14 +218,6 @@ public final class StorageSyncHelperTest { return new SignalGroupV1Record.Builder(byteArray(key), byteArray(groupId, 16)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build(); } - private static SignalGroupV1Record badGroupV1(int key, - int groupId, - boolean blocked, - boolean profileSharing) - { - return new SignalGroupV1Record.Builder(byteArray(key), byteArray(groupId, 42)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build(); - } - private static SignalGroupV2Record groupV2(int key, int groupId, boolean blocked, @@ -561,14 +226,6 @@ public final class StorageSyncHelperTest { return new SignalGroupV2Record.Builder(byteArray(key), byteArray(groupId, 32)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build(); } - private static SignalGroupV2Record badGroupV2(int key, - int groupId, - boolean blocked, - boolean profileSharing) - { - return new SignalGroupV2Record.Builder(byteArray(key), byteArray(groupId, 42)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build(); - } - private static StorageRecordUpdate update(E oldRecord, E newRecord) { return new StorageRecordUpdate<>(oldRecord, newRecord); }