Remove old Storage Service V1 code.
This commit is contained in:
parent
eb1daf4a20
commit
38e64b1f75
23 changed files with 18 additions and 2164 deletions
|
@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
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.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||||
|
@ -194,7 +194,7 @@ public class DirectoryHelper {
|
||||||
|
|
||||||
if (newRegisteredState != originalRegisteredState) {
|
if (newRegisteredState != originalRegisteredState) {
|
||||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
||||||
ApplicationDependencies.getJobManager().add(StorageSyncJob.create());
|
ApplicationDependencies.getJobManager().add(new StorageSyncJobV2());
|
||||||
|
|
||||||
if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) {
|
if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) {
|
||||||
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
|
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
|
||||||
|
|
|
@ -978,222 +978,7 @@ public class RecipientDatabase extends Database {
|
||||||
recipient.live().refresh();
|
recipient.live().refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
|
public void applyStorageSyncAccountUpdate(@NonNull StorageId storageId, SignalAccountRecord update) {
|
||||||
@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) {
|
|
||||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||||
|
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class DirectoryRefreshJob extends BaseJob {
|
||||||
boolean notifyOfNewUsers)
|
boolean notifyOfNewUsers)
|
||||||
{
|
{
|
||||||
this(new Job.Parameters.Builder()
|
this(new Job.Parameters.Builder()
|
||||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
.setQueue(StorageSyncJobV2.QUEUE_KEY)
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.setMaxAttempts(10)
|
.setMaxAttempts(10)
|
||||||
.build(),
|
.build(),
|
||||||
|
|
|
@ -141,7 +141,6 @@ public final class JobManagerFactories {
|
||||||
put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory());
|
put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory());
|
||||||
put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory());
|
put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory());
|
||||||
put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory());
|
put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory());
|
||||||
put(StorageSyncJob.KEY, new StorageSyncJob.Factory());
|
|
||||||
put(StorageSyncJobV2.KEY, new StorageSyncJobV2.Factory());
|
put(StorageSyncJobV2.KEY, new StorageSyncJobV2.Factory());
|
||||||
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||||
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
||||||
|
@ -190,6 +189,7 @@ public final class JobManagerFactories {
|
||||||
put("Argon2TestJob", new FailingJob.Factory());
|
put("Argon2TestJob", new FailingJob.Factory());
|
||||||
put("Argon2TestMigrationJob", new PassingMigrationJob.Factory());
|
put("Argon2TestMigrationJob", new PassingMigrationJob.Factory());
|
||||||
put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory());
|
put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory());
|
||||||
|
put("StorageSyncJob", new StorageSyncJobV2.Factory());
|
||||||
put("WakeGroupV2Job", new FailingJob.Factory());
|
put("WakeGroupV2Job", new FailingJob.Factory());
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ public class StorageAccountRestoreJob extends BaseJob {
|
||||||
|
|
||||||
public StorageAccountRestoreJob() {
|
public StorageAccountRestoreJob() {
|
||||||
this(new Parameters.Builder()
|
this(new Parameters.Builder()
|
||||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
.setQueue(StorageSyncJobV2.QUEUE_KEY)
|
||||||
.addConstraint(NetworkConstraint.KEY)
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
.setMaxInstancesForFactory(1)
|
.setMaxInstancesForFactory(1)
|
||||||
.setMaxAttempts(1)
|
.setMaxAttempts(1)
|
||||||
|
|
|
@ -50,7 +50,7 @@ public class StorageForcePushJob extends BaseJob {
|
||||||
|
|
||||||
public StorageForcePushJob() {
|
public StorageForcePushJob() {
|
||||||
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
||||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
.setQueue(StorageSyncJobV2.QUEUE_KEY)
|
||||||
.setMaxInstancesForFactory(1)
|
.setMaxInstancesForFactory(1)
|
||||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||||
.build());
|
.build());
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -143,7 +143,7 @@ public class StorageSyncJobV2 extends BaseJob {
|
||||||
|
|
||||||
private static final String TAG = Log.tag(StorageSyncJobV2.class);
|
private static final String TAG = Log.tag(StorageSyncJobV2.class);
|
||||||
|
|
||||||
StorageSyncJobV2() {
|
public StorageSyncJobV2() {
|
||||||
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
||||||
.setQueue(QUEUE_KEY)
|
.setQueue(QUEUE_KEY)
|
||||||
.setMaxInstancesForFactory(2)
|
.setMaxInstancesForFactory(2)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
|
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
|
||||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
import org.thoughtcrime.securesms.jobs.StorageSyncJobV2;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,12 +44,12 @@ public class StorageServiceMigrationJob extends MigrationJob {
|
||||||
|
|
||||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||||
Log.i(TAG, "Multi-device.");
|
Log.i(TAG, "Multi-device.");
|
||||||
jobManager.startChain(StorageSyncJob.create())
|
jobManager.startChain(new StorageSyncJobV2())
|
||||||
.then(new MultiDeviceKeysUpdateJob())
|
.then(new MultiDeviceKeysUpdateJob())
|
||||||
.enqueue();
|
.enqueue();
|
||||||
} else {
|
} else {
|
||||||
Log.i(TAG, "Single-device.");
|
Log.i(TAG, "Single-device.");
|
||||||
jobManager.add(StorageSyncJob.create());
|
jobManager.add(new StorageSyncJobV2());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.KbsEnclave;
|
import org.thoughtcrime.securesms.KbsEnclave;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
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.registration.service.KeyBackupSystemWrongPinException;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
@ -83,7 +83,7 @@ public class PinRestoreRepository {
|
||||||
ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
|
ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
|
||||||
stopwatch.split("AccountRestore");
|
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.split("ContactRestore");
|
||||||
|
|
||||||
stopwatch.stop(TAG);
|
stopwatch.stop(TAG);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
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.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
@ -31,7 +31,7 @@ public final class RegistrationUtil {
|
||||||
{
|
{
|
||||||
Log.i(TAG, "Marking registration completed.", new Throwable());
|
Log.i(TAG, "Marking registration completed.", new Throwable());
|
||||||
SignalStore.registrationValues().setRegistrationComplete();
|
SignalStore.registrationValues().setRegistrationComplete();
|
||||||
ApplicationDependencies.getJobManager().startChain(StorageSyncJob.create())
|
ApplicationDependencies.getJobManager().startChain(new StorageSyncJobV2())
|
||||||
.then(new DirectoryRefreshJob(false))
|
.then(new DirectoryRefreshJob(false))
|
||||||
.enqueue();
|
.enqueue();
|
||||||
} else if (!SignalStore.registrationValues().isRegistrationComplete()) {
|
} else if (!SignalStore.registrationValues().isRegistrationComplete()) {
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,7 +15,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
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.PhoneNumberPrivacyValues;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.payments.Entropy;
|
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.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
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.SignalStorageManifest;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||||
|
@ -42,7 +39,6 @@ import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -218,114 +214,6 @@ public final class StorageSyncHelper {
|
||||||
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
|
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() {
|
public static @NonNull byte[] generateKey() {
|
||||||
return keyGenerator.generate();
|
return keyGenerator.generate();
|
||||||
}
|
}
|
||||||
|
@ -335,41 +223,6 @@ public final class StorageSyncHelper {
|
||||||
keyGenerator = testKeyGenerator != null ? testKeyGenerator : KEY_GENERATOR;
|
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) {
|
public static boolean profileKeyChanged(StorageRecordUpdate<SignalContactRecord> update) {
|
||||||
return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey());
|
return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey());
|
||||||
}
|
}
|
||||||
|
@ -417,15 +270,8 @@ public final class StorageSyncHelper {
|
||||||
return SignalStorageRecord.forAccount(account);
|
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) {
|
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.setReadReceiptsEnabled(context, update.isReadReceiptsEnabled());
|
||||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, update.isTypingIndicatorsEnabled());
|
TextSecurePreferences.setTypingIndicatorsEnabled(context, update.isTypingIndicatorsEnabled());
|
||||||
|
@ -446,7 +292,7 @@ public final class StorageSyncHelper {
|
||||||
Log.d(TAG, "Registration still ongoing. Ignore sync request.");
|
Log.d(TAG, "Registration still ongoing. Ignore sync request.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ApplicationDependencies.getJobManager().add(StorageSyncJob.create());
|
ApplicationDependencies.getJobManager().add(new StorageSyncJobV2());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void scheduleRoutineSync() {
|
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 {
|
public static final class WriteOperationResult {
|
||||||
private final SignalStorageManifest manifest;
|
private final SignalStorageManifest manifest;
|
||||||
private final List<SignalStorageRecord> inserts;
|
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 MultipleExistingAccountsException extends IllegalArgumentException {}
|
||||||
private static final class InvalidAccountInsertException extends IllegalArgumentException {}
|
private static final class InvalidAccountInsertException extends IllegalArgumentException {}
|
||||||
private static final class InvalidAccountUpdateException extends IllegalArgumentException {}
|
private static final class InvalidAccountUpdateException extends IllegalArgumentException {}
|
||||||
|
|
|
@ -75,7 +75,6 @@ public final class FeatureFlags {
|
||||||
private static final String ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory";
|
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_ALARM_INTERVAL = "android.messageProcessor.alarmIntervalMins";
|
||||||
private static final String MESSAGE_PROCESSOR_DELAY = "android.messageProcessor.foregroundDelayMs";
|
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 NOTIFICATION_REWRITE = "android.notificationRewrite";
|
||||||
private static final String MP4_GIF_SEND_SUPPORT = "android.mp4GifSendSupport";
|
private static final String MP4_GIF_SEND_SUPPORT = "android.mp4GifSendSupport";
|
||||||
|
|
||||||
|
@ -109,7 +108,6 @@ public final class FeatureFlags {
|
||||||
ANIMATED_STICKER_MIN_TOTAL_MEMORY,
|
ANIMATED_STICKER_MIN_TOTAL_MEMORY,
|
||||||
MESSAGE_PROCESSOR_ALARM_INTERVAL,
|
MESSAGE_PROCESSOR_ALARM_INTERVAL,
|
||||||
MESSAGE_PROCESSOR_DELAY,
|
MESSAGE_PROCESSOR_DELAY,
|
||||||
STORAGE_SYNC_V2,
|
|
||||||
NOTIFICATION_REWRITE,
|
NOTIFICATION_REWRITE,
|
||||||
MP4_GIF_SEND_SUPPORT
|
MP4_GIF_SEND_SUPPORT
|
||||||
);
|
);
|
||||||
|
@ -155,7 +153,6 @@ public final class FeatureFlags {
|
||||||
MESSAGE_PROCESSOR_ALARM_INTERVAL,
|
MESSAGE_PROCESSOR_ALARM_INTERVAL,
|
||||||
MESSAGE_PROCESSOR_DELAY,
|
MESSAGE_PROCESSOR_DELAY,
|
||||||
GV1_FORCED_MIGRATE,
|
GV1_FORCED_MIGRATE,
|
||||||
STORAGE_SYNC_V2,
|
|
||||||
NOTIFICATION_REWRITE,
|
NOTIFICATION_REWRITE,
|
||||||
MP4_GIF_SEND_SUPPORT
|
MP4_GIF_SEND_SUPPORT
|
||||||
);
|
);
|
||||||
|
@ -344,11 +341,6 @@ public final class FeatureFlags {
|
||||||
return getInteger(ANIMATED_STICKER_MIN_TOTAL_MEMORY, (int) ByteUnit.GIGABYTES.toMegabytes(3));
|
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. */
|
/** Whether or not to use the new notification system. */
|
||||||
public static boolean useNewNotificationSystem() {
|
public static boolean useNewNotificationSystem() {
|
||||||
return getBoolean(NOTIFICATION_REWRITE, false) && Build.VERSION.SDK_INT >= 26;
|
return getBoolean(NOTIFICATION_REWRITE, false) && Build.VERSION.SDK_INT >= 26;
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,9 +15,7 @@ import org.powermock.modules.junit4.PowerMockRunnerDelegate;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult;
|
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.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
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.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -39,19 +36,15 @@ import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static junit.framework.TestCase.assertTrue;
|
import static junit.framework.TestCase.assertTrue;
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNotEquals;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.powermock.api.mockito.PowerMockito.mock;
|
import static org.powermock.api.mockito.PowerMockito.mock;
|
||||||
import static org.powermock.api.mockito.PowerMockito.mockStatic;
|
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.assertContentsEqual;
|
||||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray;
|
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray;
|
||||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteListOf;
|
import static org.thoughtcrime.securesms.testutil.TestHelpers.byteListOf;
|
||||||
import static org.thoughtcrime.securesms.testutil.TestHelpers.setOf;
|
|
||||||
|
|
||||||
@RunWith(PowerMockRunner.class)
|
@RunWith(PowerMockRunner.class)
|
||||||
@PrepareForTest({ Recipient.class, FeatureFlags.class})
|
@PrepareForTest({ Recipient.class, FeatureFlags.class})
|
||||||
|
@ -147,231 +140,6 @@ public final class StorageSyncHelperTest {
|
||||||
assertTrue(result.hasTypeMismatches());
|
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
|
@Test
|
||||||
public void ContactUpdate_equals_sameProfileKeys() {
|
public void ContactUpdate_equals_sameProfileKeys() {
|
||||||
byte[] profileKey = new byte[32];
|
byte[] profileKey = new byte[32];
|
||||||
|
@ -401,83 +169,6 @@ public final class StorageSyncHelperTest {
|
||||||
assertTrue(StorageSyncHelper.profileKeyChanged(update(a, b)));
|
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) {
|
private static Set<SignalStorageRecord> recordSetOf(SignalRecord... records) {
|
||||||
LinkedHashSet<SignalStorageRecord> storageRecords = new LinkedHashSet<>();
|
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,
|
private static SignalContactRecord.Builder contactBuilder(int key,
|
||||||
UUID uuid,
|
UUID uuid,
|
||||||
String e164,
|
String e164,
|
||||||
|
@ -521,14 +202,6 @@ public final class StorageSyncHelperTest {
|
||||||
.setGivenName(profileName);
|
.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,
|
private static SignalContactRecord contact(int key,
|
||||||
UUID uuid,
|
UUID uuid,
|
||||||
String e164,
|
String e164,
|
||||||
|
@ -545,14 +218,6 @@ public final class StorageSyncHelperTest {
|
||||||
return new SignalGroupV1Record.Builder(byteArray(key), byteArray(groupId, 16)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build();
|
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,
|
private static SignalGroupV2Record groupV2(int key,
|
||||||
int groupId,
|
int groupId,
|
||||||
boolean blocked,
|
boolean blocked,
|
||||||
|
@ -561,14 +226,6 @@ public final class StorageSyncHelperTest {
|
||||||
return new SignalGroupV2Record.Builder(byteArray(key), byteArray(groupId, 32)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build();
|
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) {
|
private static <E extends SignalRecord> StorageRecordUpdate<E> update(E oldRecord, E newRecord) {
|
||||||
return new StorageRecordUpdate<>(oldRecord, newRecord);
|
return new StorageRecordUpdate<>(oldRecord, newRecord);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue