Remove old Storage Service V1 code.

This commit is contained in:
Greyson Parrelli 2021-04-26 11:55:20 -04:00 committed by Alex Hart
parent eb1daf4a20
commit 38e64b1f75
23 changed files with 18 additions and 2164 deletions

View file

@ -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()));

View file

@ -978,222 +978,7 @@ public class RecipientDatabase extends Database {
recipient.live().refresh();
}
public boolean applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
@NonNull Collection<StorageRecordUpdate<SignalContactRecord>> contactUpdates,
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
@NonNull Collection<StorageRecordUpdate<SignalGroupV1Record>> groupV1Updates,
@NonNull Collection<SignalGroupV2Record> groupV2Inserts,
@NonNull Collection<StorageRecordUpdate<SignalGroupV2Record>> groupV2Updates)
{
SQLiteDatabase db = databaseHelper.getWritableDatabase();
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Set<RecipientId> 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<SignalContactRecord> 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<IdentityRecord> 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<IdentityRecord> 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<SignalGroupV1Record> 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<SignalGroupV2Record> update : groupV2Updates) {
ContentValues values = getValuesForStorageGroupV2(update.getNew());
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
if (updateCount < 1) {
throw new AssertionError("Had an update, but it didn't match any rows!");
}
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();

View file

@ -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(),

View file

@ -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());
}};
}

View file

@ -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)

View file

@ -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());

View file

@ -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<SignalStorageManifest> 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<StorageId> 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<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyIds());
List<SignalStorageRecord> 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<SignalStorageManifest> 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<StorageId> allLocalStorageKeys = getAllLocalStorageIds(context, self);
List<RecipientSettings> pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates();
List<RecipientSettings> pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions();
List<RecipientSettings> pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions();
Optional<SignalAccountRecord> pendingAccountInsert = StorageSyncHelper.getPendingAccountSyncInsert(context, self);
Optional<SignalAccountRecord> pendingAccountUpdate = StorageSyncHelper.getPendingAccountSyncUpdate(context, self);
Optional<LocalWriteResult> 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<SignalStorageManifest> 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<RecipientId> 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<SignalGroupV2Record> inserts)
throws IOException
{
Map<GroupId.V2, GroupId.V1> idMap = DatabaseFactory.getGroupDatabase(context).getAllExpectedV2Ids();
Iterator<SignalGroupV2Record> 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<StorageId> 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<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<StorageId> ids) {
Recipient self = Recipient.self().fresh();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
UnknownStorageIdDatabase storageKeyDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context);
List<SignalStorageRecord> 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<StorageSyncJob> {
@Override
public @NonNull StorageSyncJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new StorageSyncJob(parameters);
}
}
}

View file

@ -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)

View file

@ -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());
}
}

View file

@ -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);

View file

@ -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()) {

View file

@ -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<SignalAccountRecord> {
private static final String TAG = Log.tag(AccountConflictMerger.class);
private final Optional<SignalAccountRecord> local;
AccountConflictMerger(Optional<SignalAccountRecord> local) {
this.local = local;
}
@Override
public @NonNull Optional<SignalAccountRecord> getMatching(@NonNull SignalAccountRecord record) {
return local;
}
@Override
public @NonNull Collection<SignalAccountRecord> getInvalidEntries(@NonNull Collection<SignalAccountRecord> remoteRecords) {
Set<SignalAccountRecord> 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<PinnedConversation> 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<PinnedConversation> 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);
}
}

View file

@ -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<SignalContactRecord> {
private static final String TAG = Log.tag(ContactConflictMerger.class);
private final Map<UUID, SignalContactRecord> localByUuid = new HashMap<>();
private final Map<String, SignalContactRecord> localByE164 = new HashMap<>();
private final Recipient self;
ContactConflictMerger(@NonNull Collection<SignalContactRecord> 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<SignalContactRecord> 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<SignalContactRecord> getInvalidEntries(@NonNull Collection<SignalContactRecord> remoteRecords) {
Map<String, Set<SignalContactRecord>> localIdToRemoteRecords = new HashMap<>();
for (SignalContactRecord remote : remoteRecords) {
Optional<SignalContactRecord> local = getMatching(remote);
if (local.isPresent()) {
String serializedLocalId = Base64.encodeBytes(local.get().getId().getRaw());
Set<SignalContactRecord> matches = localIdToRemoteRecords.get(serializedLocalId);
if (matches == null) {
matches = new HashSet<>();
}
matches.add(remote);
localIdToRemoteRecords.put(serializedLocalId, matches);
}
}
Set<SignalContactRecord> duplicates = new HashSet<>();
for (Set<SignalContactRecord> matches : localIdToRemoteRecords.values()) {
if (matches.size() > 1) {
duplicates.addAll(matches);
}
}
List<SignalContactRecord> selfRecords = Stream.of(remoteRecords)
.filter(r -> r.getAddress().getUuid().equals(self.getUuid()) || r.getAddress().getNumber().equals(self.getE164()))
.toList();
Set<SignalContactRecord> 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;
}
}

View file

@ -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<SignalGroupV1Record> {
private final Map<GroupId, SignalGroupV1Record> localByGroupId;
private final GroupV2ExistenceChecker groupExistenceChecker;
GroupV1ConflictMerger(@NonNull Collection<SignalGroupV1Record> 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<SignalGroupV1Record> getMatching(@NonNull SignalGroupV1Record record) {
return Optional.fromNullable(localByGroupId.get(GroupId.v1orThrow(record.getGroupId())));
}
@Override
public @NonNull Collection<SignalGroupV1Record> getInvalidEntries(@NonNull Collection<SignalGroupV1Record> 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();
}
}
}

View file

@ -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<SignalGroupV2Record> {
private final Map<ByteString, SignalGroupV2Record> localByMasterKeyBytes;
GroupV2ConflictMerger(@NonNull Collection<SignalGroupV2Record> localOnly) {
localByMasterKeyBytes = Stream.of(localOnly).collect(Collectors.toMap((SignalGroupV2Record signalGroupV2Record) -> ByteString.copyFrom(signalGroupV2Record.getMasterKeyBytes()), g -> g));
}
@Override
public @NonNull Optional<SignalGroupV2Record> getMatching(@NonNull SignalGroupV2Record record) {
return Optional.fromNullable(localByMasterKeyBytes.get(ByteString.copyFrom(record.getMasterKeyBytes())));
}
@Override
public @NonNull Collection<SignalGroupV2Record> getInvalidEntries(@NonNull Collection<SignalGroupV2Record> 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;
}
}

View file

@ -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);
}

View file

@ -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<GroupId.V2> ids;
public StaticGroupV2ExistenceChecker(@NonNull Collection<GroupId.V2> ids) {
this.ids = new HashSet<>(ids);
}
@Override
public boolean exists(@NonNull GroupId.V2 groupId) {
return ids.contains(groupId);
}
}

View file

@ -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<SignalStorageRecord> remoteOnlyRecords,
@NonNull Collection<SignalStorageRecord> localOnlyRecords,
@NonNull GroupV2ExistenceChecker groupExistenceChecker)
{
List<SignalContactRecord> remoteOnlyContacts = Stream.of(remoteOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList();
List<SignalContactRecord> localOnlyContacts = Stream.of(localOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList();
List<SignalGroupV1Record> remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
List<SignalGroupV1Record> localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
List<SignalGroupV2Record> remoteOnlyGroupV2 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList();
List<SignalGroupV2Record> localOnlyGroupV2 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList();
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
List<SignalAccountRecord> remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
List<SignalAccountRecord> localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
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<SignalContactRecord> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self()));
RecordMergeResult<SignalGroupV1Record> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1, groupExistenceChecker));
RecordMergeResult<SignalGroupV2Record> groupV2MergeResult = resolveRecordConflict(remoteOnlyGroupV2, localOnlyGroupV2, new GroupV2ConflictMerger(localOnlyGroupV2));
RecordMergeResult<SignalAccountRecord> accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0))));
Set<SignalStorageRecord> remoteInserts = new HashSet<>();
remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList());
remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList());
remoteInserts.addAll(Stream.of(groupV2MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV2).toList());
remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList());
Set<StorageRecordUpdate<SignalStorageRecord>> 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<SignalRecord> 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<StorageId> currentLocalStorageKeys,
@NonNull MergeResult mergeResult)
{
List<SignalStorageRecord> inserts = new ArrayList<>();
inserts.addAll(mergeResult.getRemoteInserts());
inserts.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(StorageRecordUpdate::getNew).toList());
List<StorageId> 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<StorageId> 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 <E extends SignalRecord> RecordMergeResult<E> resolveRecordConflict(@NonNull Collection<E> remoteOnlyRecords,
@NonNull Collection<E> localOnlyRecords,
@NonNull ConflictMerger<E> merger)
{
Set<E> localInserts = new HashSet<>(remoteOnlyRecords);
Set<E> remoteInserts = new HashSet<>(localOnlyRecords);
Set<StorageRecordUpdate<E>> localUpdates = new HashSet<>();
Set<StorageRecordUpdate<E>> remoteUpdates = new HashSet<>();
Set<E> remoteDeletes = new HashSet<>(merger.getInvalidEntries(remoteOnlyRecords));
remoteOnlyRecords.removeAll(remoteDeletes);
localInserts.removeAll(remoteDeletes);
for (E remote : remoteOnlyRecords) {
Optional<E> 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<SignalContactRecord> 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<StorageRecordUpdate<SignalAccountRecord>> 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<SignalContactRecord> localContactInserts;
private final Set<StorageRecordUpdate<SignalContactRecord>> localContactUpdates;
private final Set<SignalGroupV1Record> localGroupV1Inserts;
private final Set<StorageRecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
private final Set<SignalGroupV2Record> localGroupV2Inserts;
private final Set<StorageRecordUpdate<SignalGroupV2Record>> localGroupV2Updates;
private final Set<SignalStorageRecord> localUnknownInserts;
private final Set<SignalStorageRecord> localUnknownDeletes;
private final Optional<StorageRecordUpdate<SignalAccountRecord>> localAccountUpdate;
private final Set<SignalStorageRecord> remoteInserts;
private final Set<StorageRecordUpdate<SignalStorageRecord>> remoteUpdates;
private final Set<SignalRecord> remoteDeletes;
@VisibleForTesting
MergeResult(@NonNull Set<SignalContactRecord> localContactInserts,
@NonNull Set<StorageRecordUpdate<SignalContactRecord>> localContactUpdates,
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
@NonNull Set<StorageRecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
@NonNull Set<SignalGroupV2Record> localGroupV2Inserts,
@NonNull Set<StorageRecordUpdate<SignalGroupV2Record>> localGroupV2Updates,
@NonNull Set<SignalStorageRecord> localUnknownInserts,
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
@NonNull Optional<StorageRecordUpdate<SignalAccountRecord>> localAccountUpdate,
@NonNull Set<SignalStorageRecord> remoteInserts,
@NonNull Set<StorageRecordUpdate<SignalStorageRecord>> remoteUpdates,
@NonNull Set<SignalRecord> 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<SignalContactRecord> getLocalContactInserts() {
return localContactInserts;
}
public @NonNull Set<StorageRecordUpdate<SignalContactRecord>> getLocalContactUpdates() {
return localContactUpdates;
}
public @NonNull Set<SignalGroupV1Record> getLocalGroupV1Inserts() {
return localGroupV1Inserts;
}
public @NonNull Set<StorageRecordUpdate<SignalGroupV1Record>> getLocalGroupV1Updates() {
return localGroupV1Updates;
}
public @NonNull Set<SignalGroupV2Record> getLocalGroupV2Inserts() {
return localGroupV2Inserts;
}
public @NonNull Set<StorageRecordUpdate<SignalGroupV2Record>> getLocalGroupV2Updates() {
return localGroupV2Updates;
}
public @NonNull Set<SignalStorageRecord> getLocalUnknownInserts() {
return localUnknownInserts;
}
public @NonNull Set<SignalStorageRecord> getLocalUnknownDeletes() {
return localUnknownDeletes;
}
public @NonNull Optional<StorageRecordUpdate<SignalAccountRecord>> getLocalAccountUpdate() {
return localAccountUpdate;
}
public @NonNull Set<SignalStorageRecord> getRemoteInserts() {
return remoteInserts;
}
public @NonNull Set<StorageRecordUpdate<SignalStorageRecord>> getRemoteUpdates() {
return remoteUpdates;
}
public @NonNull Set<SignalRecord> getRemoteDeletes() {
return remoteDeletes;
}
@NonNull Set<SignalRecord> getAllNewRecords() {
Set<SignalRecord> 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<SignalRecord> getAllRemovedRecords() {
Set<SignalRecord> 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<SignalStorageRecord> inserts;
@ -692,33 +409,6 @@ public final class StorageSyncHelper {
}
}
private static class RecordMergeResult<Record extends SignalRecord> {
final Set<Record> localInserts;
final Set<StorageRecordUpdate<Record>> localUpdates;
final Set<Record> remoteInserts;
final Set<StorageRecordUpdate<Record>> remoteUpdates;
final Set<Record> remoteDeletes;
RecordMergeResult(@NonNull Set<Record> localInserts,
@NonNull Set<StorageRecordUpdate<Record>> localUpdates,
@NonNull Set<Record> remoteInserts,
@NonNull Set<StorageRecordUpdate<Record>> remoteUpdates,
@NonNull Set<Record> remoteDeletes)
{
this.localInserts = localInserts;
this.localUpdates = localUpdates;
this.remoteInserts = remoteInserts;
this.remoteUpdates = remoteUpdates;
this.remoteDeletes = remoteDeletes;
}
}
interface ConflictMerger<E extends SignalRecord> {
@NonNull Optional<E> getMatching(@NonNull E record);
@NonNull Collection<E> getInvalidEntries(@NonNull Collection<E> 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 {}

View file

@ -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;

View file

@ -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<SignalContactRecord> 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<SignalContactRecord> 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<SignalContactRecord> invalid = new ContactConflictMerger(Collections.singleton(aLocal), SELF).getInvalidEntries(setOf(aRemote1, aRemote2, aRemote3, bRemote));
assertContentsEqual(setOf(aRemote1, aRemote2, aRemote3), invalid);
}
}

View file

@ -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<SignalGroupV1Record> 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<SignalGroupV1Record> 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);
}
}

View file

@ -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<SignalGroupV2Record> 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);
}
}

View file

@ -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<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, remote6, unknownRemote);
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, local5, local6, unknownLocal);
MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly, 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<StorageId> localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100), groupV2Key(200));
SignalContactRecord insert1 = contact(6, UUID_A, E164_A, "a");
SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b");
SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z");
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<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
Set<SignalStorageRecord> 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<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
Set<SignalStorageRecord> 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<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
Set<SignalStorageRecord> 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<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
Set<SignalStorageRecord> 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<SignalStorageRecord> remoteOnly = recordSetOf(remoteAccount);
Set<SignalStorageRecord> 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<SignalStorageRecord> recordSetOf(SignalRecord... records) {
LinkedHashSet<SignalStorageRecord> storageRecords = new LinkedHashSet<>();
@ -502,16 +193,6 @@ public final class StorageSyncHelperTest {
}
}
private static Set<SignalStorageRecord> recordSetOf(SignalGroupV1Record... groupRecords) {
LinkedHashSet<SignalStorageRecord> 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 <E extends SignalRecord> StorageRecordUpdate<E> update(E oldRecord, E newRecord) {
return new StorageRecordUpdate<>(oldRecord, newRecord);
}