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;