mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 02:10:44 +01:00
Remove old Storage Service V1 code.
This commit is contained in:
committed by
Alex Hart
parent
eb1daf4a20
commit
38e64b1f75
@@ -32,7 +32,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJobV2;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
@@ -194,7 +194,7 @@ public class DirectoryHelper {
|
||||
|
||||
if (newRegisteredState != originalRegisteredState) {
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
|
||||
ApplicationDependencies.getJobManager().add(StorageSyncJob.create());
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJobV2());
|
||||
|
||||
if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) {
|
||||
notifyNewUsers(context, Collections.singletonList(recipient.getId()));
|
||||
|
||||
@@ -978,222 +978,7 @@ public class RecipientDatabase extends Database {
|
||||
recipient.live().refresh();
|
||||
}
|
||||
|
||||
public boolean applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
|
||||
@NonNull Collection<StorageRecordUpdate<SignalContactRecord>> contactUpdates,
|
||||
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
|
||||
@NonNull Collection<StorageRecordUpdate<SignalGroupV1Record>> groupV1Updates,
|
||||
@NonNull Collection<SignalGroupV2Record> groupV2Inserts,
|
||||
@NonNull Collection<StorageRecordUpdate<SignalGroupV2Record>> groupV2Updates)
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
Set<RecipientId> needsRefresh = new HashSet<>();
|
||||
boolean forcePush = false;
|
||||
|
||||
db.beginTransaction();
|
||||
|
||||
try {
|
||||
for (SignalContactRecord insert : contactInserts) {
|
||||
ContentValues values = getValuesForStorageContact(insert, true);
|
||||
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
RecipientId recipientId = null;
|
||||
|
||||
if (id < 0) {
|
||||
values = getValuesForStorageContact(insert, false);
|
||||
Log.w(TAG, "Failed to insert! It's likely that these were newly-registered users that were missed in the merge. Doing an update instead.");
|
||||
|
||||
if (insert.getAddress().getNumber().isPresent()) {
|
||||
try {
|
||||
int count = db.update(TABLE_NAME, values, PHONE + " = ?", new String[] { insert.getAddress().getNumber().get() });
|
||||
Log.w(TAG, "Updated " + count + " users by E164.");
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Failed to update the UUID on an existing E164 user. Possibly merging.");
|
||||
recipientId = getAndPossiblyMerge(insert.getAddress().getUuid().get(), insert.getAddress().getNumber().get(), true);
|
||||
Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Resulting id: " + recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientId == null && insert.getAddress().getUuid().isPresent()) {
|
||||
try {
|
||||
int count = db.update(TABLE_NAME, values, UUID + " = ?", new String[] { insert.getAddress().getUuid().get().toString() });
|
||||
Log.w(TAG, "Updated " + count + " users by UUID.");
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Failed to update the E164 on an existing UUID user. Possibly merging.");
|
||||
recipientId = getAndPossiblyMerge(insert.getAddress().getUuid().get(), insert.getAddress().getNumber().get(), true);
|
||||
Log.w(TAG, "[applyStorageSyncUpdates -- Insert] Resulting id: " + recipientId);
|
||||
}
|
||||
}
|
||||
|
||||
if (recipientId == null && insert.getAddress().getNumber().isPresent()) {
|
||||
recipientId = getByE164(insert.getAddress().getNumber().get()).orNull();
|
||||
}
|
||||
|
||||
if (recipientId == null && insert.getAddress().getUuid().isPresent()) {
|
||||
recipientId = getByUuid(insert.getAddress().getUuid().get()).orNull();
|
||||
}
|
||||
|
||||
if (recipientId == null) {
|
||||
Log.w(TAG, "Failed to recover from a failed insert!");
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
recipientId = RecipientId.from(id);
|
||||
}
|
||||
|
||||
if (insert.getIdentityKey().isPresent()) {
|
||||
try {
|
||||
IdentityKey identityKey = new IdentityKey(insert.getIdentityKey().get(), 0);
|
||||
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.getIdentityState()));
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e);
|
||||
}
|
||||
}
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipientId, insert);
|
||||
needsRefresh.add(recipientId);
|
||||
}
|
||||
|
||||
for (StorageRecordUpdate<SignalContactRecord> update : contactUpdates) {
|
||||
ContentValues values = getValuesForStorageContact(update.getNew(), false);
|
||||
|
||||
try {
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
} catch (SQLiteConstraintException e) {
|
||||
Log.w(TAG, "[applyStorageSyncUpdates -- Update] Failed to update a user by storageId.");
|
||||
|
||||
RecipientId recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getOld().getId().getRaw())).get();
|
||||
Log.w(TAG, "[applyStorageSyncUpdates -- Update] Found user " + recipientId + ". Possibly merging.");
|
||||
|
||||
recipientId = getAndPossiblyMerge(update.getNew().getAddress().getUuid().orNull(), update.getNew().getAddress().getNumber().orNull(), true);
|
||||
Log.w(TAG, "[applyStorageSyncUpdates -- Update] Merged into " + recipientId);
|
||||
|
||||
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId));
|
||||
}
|
||||
|
||||
RecipientId recipientId = getByStorageKeyOrThrow(update.getNew().getId().getRaw());
|
||||
|
||||
if (StorageSyncHelper.profileKeyChanged(update)) {
|
||||
ContentValues clearValues = new ContentValues(1);
|
||||
clearValues.putNull(PROFILE_KEY_CREDENTIAL);
|
||||
update(recipientId, clearValues);
|
||||
}
|
||||
|
||||
try {
|
||||
Optional<IdentityRecord> oldIdentityRecord = identityDatabase.getIdentity(recipientId);
|
||||
|
||||
if (update.getNew().getIdentityKey().isPresent()) {
|
||||
IdentityKey identityKey = new IdentityKey(update.getNew().getIdentityKey().get(), 0);
|
||||
DatabaseFactory.getIdentityDatabase(context).updateIdentityAfterSync(recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.getNew().getIdentityState()));
|
||||
}
|
||||
|
||||
Optional<IdentityRecord> newIdentityRecord = identityDatabase.getIdentity(recipientId);
|
||||
|
||||
if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED) &&
|
||||
(!oldIdentityRecord.isPresent() || oldIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED))
|
||||
{
|
||||
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
|
||||
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) &&
|
||||
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
|
||||
{
|
||||
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true);
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w(TAG, "Failed to process identity key during update! Skipping.", e);
|
||||
}
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipientId, update.getNew());
|
||||
needsRefresh.add(recipientId);
|
||||
}
|
||||
|
||||
for (SignalGroupV1Record insert : groupV1Inserts) {
|
||||
long id = db.insertWithOnConflict(TABLE_NAME, null, getValuesForStorageGroupV1(insert), SQLiteDatabase.CONFLICT_IGNORE);
|
||||
|
||||
if (id < 0) {
|
||||
Log.w(TAG, "Duplicate GV1 entry detected! Ignoring, suggesting force-push.");
|
||||
forcePush = true;
|
||||
} else {
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(insert.getGroupId()));
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
|
||||
needsRefresh.add(recipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
for (StorageRecordUpdate<SignalGroupV1Record> update : groupV1Updates) {
|
||||
ContentValues values = getValuesForStorageGroupV1(update.getNew());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
|
||||
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v1orThrow(update.getOld().getGroupId()));
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
||||
needsRefresh.add(recipient.getId());
|
||||
}
|
||||
|
||||
for (SignalGroupV2Record insert : groupV2Inserts) {
|
||||
GroupMasterKey masterKey = insert.getMasterKeyOrThrow();
|
||||
GroupId.V2 groupId = GroupId.v2(masterKey);
|
||||
ContentValues values = getValuesForStorageGroupV2(insert);
|
||||
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
|
||||
Recipient recipient = Recipient.externalGroupExact(context, groupId);
|
||||
|
||||
if (id < 0) {
|
||||
Log.w(TAG, String.format("Recipient %s is already linked to group %s", recipient.getId(), groupId));
|
||||
} else {
|
||||
Log.i(TAG, String.format("Inserted recipient %s for group %s", recipient.getId(), groupId));
|
||||
}
|
||||
|
||||
Log.i(TAG, "Creating restore placeholder for " + groupId);
|
||||
DatabaseFactory.getGroupDatabase(context)
|
||||
.create(masterKey,
|
||||
DecryptedGroup.newBuilder()
|
||||
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
|
||||
.build());
|
||||
|
||||
Log.i(TAG, "Scheduling request for latest group info for " + groupId);
|
||||
|
||||
ApplicationDependencies.getJobManager().add(new RequestGroupV2InfoJob(groupId));
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), insert);
|
||||
needsRefresh.add(recipient.getId());
|
||||
}
|
||||
|
||||
for (StorageRecordUpdate<SignalGroupV2Record> update : groupV2Updates) {
|
||||
ContentValues values = getValuesForStorageGroupV2(update.getNew());
|
||||
int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(update.getOld().getId().getRaw())});
|
||||
|
||||
if (updateCount < 1) {
|
||||
throw new AssertionError("Had an update, but it didn't match any rows!");
|
||||
}
|
||||
|
||||
GroupMasterKey masterKey = update.getOld().getMasterKeyOrThrow();
|
||||
Recipient recipient = Recipient.externalGroupExact(context, GroupId.v2(masterKey));
|
||||
|
||||
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
|
||||
needsRefresh.add(recipient.getId());
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
for (RecipientId id : needsRefresh) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
|
||||
return forcePush;
|
||||
}
|
||||
|
||||
public void applyStorageSyncUpdates(@NonNull StorageId storageId, SignalAccountRecord update) {
|
||||
public void applyStorageSyncAccountUpdate(@NonNull StorageId storageId, SignalAccountRecord update) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
@@ -34,7 +34,7 @@ public class DirectoryRefreshJob extends BaseJob {
|
||||
boolean notifyOfNewUsers)
|
||||
{
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.setQueue(StorageSyncJobV2.QUEUE_KEY)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxAttempts(10)
|
||||
.build(),
|
||||
|
||||
@@ -141,7 +141,6 @@ public final class JobManagerFactories {
|
||||
put(StickerDownloadJob.KEY, new StickerDownloadJob.Factory());
|
||||
put(StickerPackDownloadJob.KEY, new StickerPackDownloadJob.Factory());
|
||||
put(StorageForcePushJob.KEY, new StorageForcePushJob.Factory());
|
||||
put(StorageSyncJob.KEY, new StorageSyncJob.Factory());
|
||||
put(StorageSyncJobV2.KEY, new StorageSyncJobV2.Factory());
|
||||
put(TrimThreadJob.KEY, new TrimThreadJob.Factory());
|
||||
put(TypingSendJob.KEY, new TypingSendJob.Factory());
|
||||
@@ -190,6 +189,7 @@ public final class JobManagerFactories {
|
||||
put("Argon2TestJob", new FailingJob.Factory());
|
||||
put("Argon2TestMigrationJob", new PassingMigrationJob.Factory());
|
||||
put("StorageKeyRotationMigrationJob", new PassingMigrationJob.Factory());
|
||||
put("StorageSyncJob", new StorageSyncJobV2.Factory());
|
||||
put("WakeGroupV2Job", new FailingJob.Factory());
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public class StorageAccountRestoreJob extends BaseJob {
|
||||
|
||||
public StorageAccountRestoreJob() {
|
||||
this(new Parameters.Builder()
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.setQueue(StorageSyncJobV2.QUEUE_KEY)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.setMaxAttempts(1)
|
||||
|
||||
@@ -50,7 +50,7 @@ public class StorageForcePushJob extends BaseJob {
|
||||
|
||||
public StorageForcePushJob() {
|
||||
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(StorageSyncJob.QUEUE_KEY)
|
||||
.setQueue(StorageSyncJobV2.QUEUE_KEY)
|
||||
.setMaxInstancesForFactory(1)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.build());
|
||||
|
||||
@@ -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);
|
||||
|
||||
StorageSyncJobV2() {
|
||||
public StorageSyncJobV2() {
|
||||
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue(QUEUE_KEY)
|
||||
.setMaxInstancesForFactory(2)
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJobV2;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
/**
|
||||
@@ -44,12 +44,12 @@ public class StorageServiceMigrationJob extends MigrationJob {
|
||||
|
||||
if (TextSecurePreferences.isMultiDevice(context)) {
|
||||
Log.i(TAG, "Multi-device.");
|
||||
jobManager.startChain(StorageSyncJob.create())
|
||||
jobManager.startChain(new StorageSyncJobV2())
|
||||
.then(new MultiDeviceKeysUpdateJob())
|
||||
.enqueue();
|
||||
} else {
|
||||
Log.i(TAG, "Single-device.");
|
||||
jobManager.add(StorageSyncJob.create());
|
||||
jobManager.add(new StorageSyncJobV2());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.KbsEnclave;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJobV2;
|
||||
import org.thoughtcrime.securesms.registration.service.KeyBackupSystemWrongPinException;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@@ -83,7 +83,7 @@ public class PinRestoreRepository {
|
||||
ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
|
||||
stopwatch.split("AccountRestore");
|
||||
|
||||
ApplicationDependencies.getJobManager().runSynchronously(StorageSyncJob.create(), TimeUnit.SECONDS.toMillis(10));
|
||||
ApplicationDependencies.getJobManager().runSynchronously(new StorageSyncJobV2(), TimeUnit.SECONDS.toMillis(10));
|
||||
stopwatch.split("ContactRestore");
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
@@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJobV2;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -31,7 +31,7 @@ public final class RegistrationUtil {
|
||||
{
|
||||
Log.i(TAG, "Marking registration completed.", new Throwable());
|
||||
SignalStore.registrationValues().setRegistrationComplete();
|
||||
ApplicationDependencies.getJobManager().startChain(StorageSyncJob.create())
|
||||
ApplicationDependencies.getJobManager().startChain(new StorageSyncJobV2())
|
||||
.then(new DirectoryRefreshJob(false))
|
||||
.enqueue();
|
||||
} else if (!SignalStore.registrationValues().isRegistrationComplete()) {
|
||||
|
||||
@@ -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.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.StorageSyncJobV2;
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.payments.Entropy;
|
||||
@@ -28,9 +28,6 @@ import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
|
||||
import org.whispersystems.signalservice.api.storage.SignalRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
||||
@@ -42,7 +39,6 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -218,114 +214,6 @@ public final class StorageSyncHelper {
|
||||
return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two sets of storage records, this will resolve the data into a set of actions that need
|
||||
* to be applied to resolve the differences. This will handle discovering which records between
|
||||
* the two collections refer to the same contacts and are actually updates, which are brand new,
|
||||
* etc.
|
||||
*
|
||||
* @param remoteOnlyRecords Records that are only present remotely.
|
||||
* @param localOnlyRecords Records that are only present locally.
|
||||
*
|
||||
* @return A set of actions that should be applied to resolve the conflict.
|
||||
*/
|
||||
public static @NonNull MergeResult resolveConflict(@NonNull Collection<SignalStorageRecord> remoteOnlyRecords,
|
||||
@NonNull Collection<SignalStorageRecord> localOnlyRecords,
|
||||
@NonNull GroupV2ExistenceChecker groupExistenceChecker)
|
||||
{
|
||||
List<SignalContactRecord> remoteOnlyContacts = Stream.of(remoteOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList();
|
||||
List<SignalContactRecord> localOnlyContacts = Stream.of(localOnlyRecords).filter(r -> r.getContact().isPresent()).map(r -> r.getContact().get()).toList();
|
||||
|
||||
List<SignalGroupV1Record> remoteOnlyGroupV1 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
|
||||
List<SignalGroupV1Record> localOnlyGroupV1 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV1().isPresent()).map(r -> r.getGroupV1().get()).toList();
|
||||
|
||||
List<SignalGroupV2Record> remoteOnlyGroupV2 = Stream.of(remoteOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList();
|
||||
List<SignalGroupV2Record> localOnlyGroupV2 = Stream.of(localOnlyRecords).filter(r -> r.getGroupV2().isPresent()).map(r -> r.getGroupV2().get()).toList();
|
||||
|
||||
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
|
||||
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(SignalStorageRecord::isUnknown).toList();
|
||||
|
||||
List<SignalAccountRecord> remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
|
||||
List<SignalAccountRecord> localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
|
||||
if (remoteOnlyAccount.size() > 0 && localOnlyAccount.isEmpty()) {
|
||||
throw new AssertionError("Found a remote-only account, but no local-only account!");
|
||||
}
|
||||
if (localOnlyAccount.size() > 1) {
|
||||
throw new AssertionError("Multiple local accounts?");
|
||||
}
|
||||
|
||||
RecordMergeResult<SignalContactRecord> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self()));
|
||||
RecordMergeResult<SignalGroupV1Record> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1, groupExistenceChecker));
|
||||
RecordMergeResult<SignalGroupV2Record> groupV2MergeResult = resolveRecordConflict(remoteOnlyGroupV2, localOnlyGroupV2, new GroupV2ConflictMerger(localOnlyGroupV2));
|
||||
RecordMergeResult<SignalAccountRecord> accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0))));
|
||||
|
||||
Set<SignalStorageRecord> remoteInserts = new HashSet<>();
|
||||
remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList());
|
||||
remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList());
|
||||
remoteInserts.addAll(Stream.of(groupV2MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV2).toList());
|
||||
remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList());
|
||||
|
||||
Set<StorageRecordUpdate<SignalStorageRecord>> remoteUpdates = new HashSet<>();
|
||||
remoteUpdates.addAll(Stream.of(contactMergeResult.remoteUpdates)
|
||||
.map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forContact(c.getOld()), SignalStorageRecord.forContact(c.getNew())))
|
||||
.toList());
|
||||
remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates)
|
||||
.map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew())))
|
||||
.toList());
|
||||
remoteUpdates.addAll(Stream.of(groupV2MergeResult.remoteUpdates)
|
||||
.map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forGroupV2(c.getOld()), SignalStorageRecord.forGroupV2(c.getNew())))
|
||||
.toList());
|
||||
remoteUpdates.addAll(Stream.of(accountMergeResult.remoteUpdates)
|
||||
.map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew())))
|
||||
.toList());
|
||||
|
||||
Set<SignalRecord> remoteDeletes = new HashSet<>();
|
||||
remoteDeletes.addAll(contactMergeResult.remoteDeletes);
|
||||
remoteDeletes.addAll(groupV1MergeResult.remoteDeletes);
|
||||
remoteDeletes.addAll(groupV2MergeResult.remoteDeletes);
|
||||
remoteDeletes.addAll(accountMergeResult.remoteDeletes);
|
||||
|
||||
return new MergeResult(contactMergeResult.localInserts,
|
||||
contactMergeResult.localUpdates,
|
||||
groupV1MergeResult.localInserts,
|
||||
groupV1MergeResult.localUpdates,
|
||||
groupV2MergeResult.localInserts,
|
||||
groupV2MergeResult.localUpdates,
|
||||
new LinkedHashSet<>(remoteOnlyUnknowns),
|
||||
new LinkedHashSet<>(localOnlyUnknowns),
|
||||
accountMergeResult.localUpdates.isEmpty() ? Optional.absent() : Optional.of(accountMergeResult.localUpdates.iterator().next()),
|
||||
remoteInserts,
|
||||
remoteUpdates,
|
||||
remoteDeletes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes that the merge result has *not* yet been applied to the local data. That means that
|
||||
* this method will handle generating the correct final key set based on the merge result.
|
||||
*/
|
||||
public static @NonNull WriteOperationResult createWriteOperation(long currentManifestVersion,
|
||||
@NonNull List<StorageId> currentLocalStorageKeys,
|
||||
@NonNull MergeResult mergeResult)
|
||||
{
|
||||
List<SignalStorageRecord> inserts = new ArrayList<>();
|
||||
inserts.addAll(mergeResult.getRemoteInserts());
|
||||
inserts.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(StorageRecordUpdate::getNew).toList());
|
||||
|
||||
List<StorageId> deletes = new ArrayList<>();
|
||||
deletes.addAll(Stream.of(mergeResult.getRemoteDeletes()).map(SignalRecord::getId).toList());
|
||||
deletes.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(StorageRecordUpdate::getOld).map(SignalStorageRecord::getId).toList());
|
||||
|
||||
Set<StorageId> completeKeys = new HashSet<>(currentLocalStorageKeys);
|
||||
completeKeys.addAll(Stream.of(mergeResult.getAllNewRecords()).map(SignalRecord::getId).toList());
|
||||
completeKeys.removeAll(Stream.of(mergeResult.getAllRemovedRecords()).map(SignalRecord::getId).toList());
|
||||
completeKeys.addAll(Stream.of(inserts).map(SignalStorageRecord::getId).toList());
|
||||
completeKeys.removeAll(deletes);
|
||||
|
||||
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, new ArrayList<>(completeKeys));
|
||||
|
||||
return new WriteOperationResult(manifest, inserts, Stream.of(deletes).map(StorageId::getRaw).toList());
|
||||
}
|
||||
|
||||
public static @NonNull byte[] generateKey() {
|
||||
return keyGenerator.generate();
|
||||
}
|
||||
@@ -335,41 +223,6 @@ public final class StorageSyncHelper {
|
||||
keyGenerator = testKeyGenerator != null ? testKeyGenerator : KEY_GENERATOR;
|
||||
}
|
||||
|
||||
private static @NonNull <E extends SignalRecord> RecordMergeResult<E> resolveRecordConflict(@NonNull Collection<E> remoteOnlyRecords,
|
||||
@NonNull Collection<E> localOnlyRecords,
|
||||
@NonNull ConflictMerger<E> merger)
|
||||
{
|
||||
Set<E> localInserts = new HashSet<>(remoteOnlyRecords);
|
||||
Set<E> remoteInserts = new HashSet<>(localOnlyRecords);
|
||||
Set<StorageRecordUpdate<E>> localUpdates = new HashSet<>();
|
||||
Set<StorageRecordUpdate<E>> remoteUpdates = new HashSet<>();
|
||||
Set<E> remoteDeletes = new HashSet<>(merger.getInvalidEntries(remoteOnlyRecords));
|
||||
|
||||
remoteOnlyRecords.removeAll(remoteDeletes);
|
||||
localInserts.removeAll(remoteDeletes);
|
||||
|
||||
for (E remote : remoteOnlyRecords) {
|
||||
Optional<E> local = merger.getMatching(remote);
|
||||
|
||||
if (local.isPresent()) {
|
||||
E merged = merger.merge(remote, local.get(), keyGenerator);
|
||||
|
||||
if (!merged.equals(remote)) {
|
||||
remoteUpdates.add(new StorageRecordUpdate<>(remote, merged));
|
||||
}
|
||||
|
||||
if (!merged.equals(local.get())) {
|
||||
localUpdates.add(new StorageRecordUpdate<>(local.get(), merged));
|
||||
}
|
||||
|
||||
localInserts.remove(remote);
|
||||
remoteInserts.remove(local.get());
|
||||
}
|
||||
}
|
||||
|
||||
return new RecordMergeResult<>(localInserts, localUpdates, remoteInserts, remoteUpdates, remoteDeletes);
|
||||
}
|
||||
|
||||
public static boolean profileKeyChanged(StorageRecordUpdate<SignalContactRecord> update) {
|
||||
return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey());
|
||||
}
|
||||
@@ -417,15 +270,8 @@ public final class StorageSyncHelper {
|
||||
return SignalStorageRecord.forAccount(account);
|
||||
}
|
||||
|
||||
public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional<StorageRecordUpdate<SignalAccountRecord>> update) {
|
||||
if (!update.isPresent()) {
|
||||
return;
|
||||
}
|
||||
applyAccountStorageSyncUpdates(context, Recipient.self(), update.get().getNew(), true);
|
||||
}
|
||||
|
||||
public static void applyAccountStorageSyncUpdates(@NonNull Context context, @NonNull Recipient self, @NonNull SignalAccountRecord update, boolean fetchProfile) {
|
||||
DatabaseFactory.getRecipientDatabase(context).applyStorageSyncUpdates(StorageId.forAccount(self.getStorageServiceId()), update);
|
||||
DatabaseFactory.getRecipientDatabase(context).applyStorageSyncAccountUpdate(StorageId.forAccount(self.getStorageServiceId()), update);
|
||||
|
||||
TextSecurePreferences.setReadReceiptsEnabled(context, update.isReadReceiptsEnabled());
|
||||
TextSecurePreferences.setTypingIndicatorsEnabled(context, update.isTypingIndicatorsEnabled());
|
||||
@@ -446,7 +292,7 @@ public final class StorageSyncHelper {
|
||||
Log.d(TAG, "Registration still ongoing. Ignore sync request.");
|
||||
return;
|
||||
}
|
||||
ApplicationDependencies.getJobManager().add(StorageSyncJob.create());
|
||||
ApplicationDependencies.getJobManager().add(new StorageSyncJobV2());
|
||||
}
|
||||
|
||||
public static void scheduleRoutineSync() {
|
||||
@@ -500,135 +346,6 @@ public final class StorageSyncHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MergeResult {
|
||||
private final Set<SignalContactRecord> localContactInserts;
|
||||
private final Set<StorageRecordUpdate<SignalContactRecord>> localContactUpdates;
|
||||
private final Set<SignalGroupV1Record> localGroupV1Inserts;
|
||||
private final Set<StorageRecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
|
||||
private final Set<SignalGroupV2Record> localGroupV2Inserts;
|
||||
private final Set<StorageRecordUpdate<SignalGroupV2Record>> localGroupV2Updates;
|
||||
private final Set<SignalStorageRecord> localUnknownInserts;
|
||||
private final Set<SignalStorageRecord> localUnknownDeletes;
|
||||
private final Optional<StorageRecordUpdate<SignalAccountRecord>> localAccountUpdate;
|
||||
private final Set<SignalStorageRecord> remoteInserts;
|
||||
private final Set<StorageRecordUpdate<SignalStorageRecord>> remoteUpdates;
|
||||
private final Set<SignalRecord> remoteDeletes;
|
||||
|
||||
@VisibleForTesting
|
||||
MergeResult(@NonNull Set<SignalContactRecord> localContactInserts,
|
||||
@NonNull Set<StorageRecordUpdate<SignalContactRecord>> localContactUpdates,
|
||||
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
|
||||
@NonNull Set<StorageRecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
|
||||
@NonNull Set<SignalGroupV2Record> localGroupV2Inserts,
|
||||
@NonNull Set<StorageRecordUpdate<SignalGroupV2Record>> localGroupV2Updates,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownInserts,
|
||||
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
|
||||
@NonNull Optional<StorageRecordUpdate<SignalAccountRecord>> localAccountUpdate,
|
||||
@NonNull Set<SignalStorageRecord> remoteInserts,
|
||||
@NonNull Set<StorageRecordUpdate<SignalStorageRecord>> remoteUpdates,
|
||||
@NonNull Set<SignalRecord> remoteDeletes)
|
||||
{
|
||||
this.localContactInserts = localContactInserts;
|
||||
this.localContactUpdates = localContactUpdates;
|
||||
this.localGroupV1Inserts = localGroupV1Inserts;
|
||||
this.localGroupV1Updates = localGroupV1Updates;
|
||||
this.localGroupV2Inserts = localGroupV2Inserts;
|
||||
this.localGroupV2Updates = localGroupV2Updates;
|
||||
this.localUnknownInserts = localUnknownInserts;
|
||||
this.localUnknownDeletes = localUnknownDeletes;
|
||||
this.localAccountUpdate = localAccountUpdate;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
this.remoteDeletes = remoteDeletes;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalContactRecord> getLocalContactInserts() {
|
||||
return localContactInserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<StorageRecordUpdate<SignalContactRecord>> getLocalContactUpdates() {
|
||||
return localContactUpdates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalGroupV1Record> getLocalGroupV1Inserts() {
|
||||
return localGroupV1Inserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<StorageRecordUpdate<SignalGroupV1Record>> getLocalGroupV1Updates() {
|
||||
return localGroupV1Updates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalGroupV2Record> getLocalGroupV2Inserts() {
|
||||
return localGroupV2Inserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<StorageRecordUpdate<SignalGroupV2Record>> getLocalGroupV2Updates() {
|
||||
return localGroupV2Updates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getLocalUnknownInserts() {
|
||||
return localUnknownInserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getLocalUnknownDeletes() {
|
||||
return localUnknownDeletes;
|
||||
}
|
||||
|
||||
public @NonNull Optional<StorageRecordUpdate<SignalAccountRecord>> getLocalAccountUpdate() {
|
||||
return localAccountUpdate;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalStorageRecord> getRemoteInserts() {
|
||||
return remoteInserts;
|
||||
}
|
||||
|
||||
public @NonNull Set<StorageRecordUpdate<SignalStorageRecord>> getRemoteUpdates() {
|
||||
return remoteUpdates;
|
||||
}
|
||||
|
||||
public @NonNull Set<SignalRecord> getRemoteDeletes() {
|
||||
return remoteDeletes;
|
||||
}
|
||||
|
||||
@NonNull Set<SignalRecord> getAllNewRecords() {
|
||||
Set<SignalRecord> records = new HashSet<>();
|
||||
|
||||
records.addAll(localContactInserts);
|
||||
records.addAll(localGroupV1Inserts);
|
||||
records.addAll(localGroupV2Inserts);
|
||||
records.addAll(remoteInserts);
|
||||
records.addAll(localUnknownInserts);
|
||||
records.addAll(Stream.of(localContactUpdates).map(StorageRecordUpdate::getNew).toList());
|
||||
records.addAll(Stream.of(localGroupV1Updates).map(StorageRecordUpdate::getNew).toList());
|
||||
records.addAll(Stream.of(localGroupV2Updates).map(StorageRecordUpdate::getNew).toList());
|
||||
records.addAll(Stream.of(remoteUpdates).map(StorageRecordUpdate::getNew).toList());
|
||||
if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getNew());
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@NonNull Set<SignalRecord> getAllRemovedRecords() {
|
||||
Set<SignalRecord> records = new HashSet<>();
|
||||
|
||||
records.addAll(localUnknownDeletes);
|
||||
records.addAll(Stream.of(localContactUpdates).map(StorageRecordUpdate::getOld).toList());
|
||||
records.addAll(Stream.of(localGroupV1Updates).map(StorageRecordUpdate::getOld).toList());
|
||||
records.addAll(Stream.of(localGroupV2Updates).map(StorageRecordUpdate::getOld).toList());
|
||||
records.addAll(Stream.of(remoteUpdates).map(StorageRecordUpdate::getOld).toList());
|
||||
records.addAll(remoteDeletes);
|
||||
if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getOld());
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return String.format(Locale.ENGLISH,
|
||||
"localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localGroupV2Inserts: %d, localGroupV2Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d, remoteDeletes: %d",
|
||||
localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localGroupV2Inserts.size(), localGroupV2Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size(), remoteDeletes.size());
|
||||
}
|
||||
}
|
||||
|
||||
public static final class WriteOperationResult {
|
||||
private final SignalStorageManifest manifest;
|
||||
private final List<SignalStorageRecord> inserts;
|
||||
@@ -692,33 +409,6 @@ public final class StorageSyncHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static class RecordMergeResult<Record extends SignalRecord> {
|
||||
final Set<Record> localInserts;
|
||||
final Set<StorageRecordUpdate<Record>> localUpdates;
|
||||
final Set<Record> remoteInserts;
|
||||
final Set<StorageRecordUpdate<Record>> remoteUpdates;
|
||||
final Set<Record> remoteDeletes;
|
||||
|
||||
RecordMergeResult(@NonNull Set<Record> localInserts,
|
||||
@NonNull Set<StorageRecordUpdate<Record>> localUpdates,
|
||||
@NonNull Set<Record> remoteInserts,
|
||||
@NonNull Set<StorageRecordUpdate<Record>> remoteUpdates,
|
||||
@NonNull Set<Record> remoteDeletes)
|
||||
{
|
||||
this.localInserts = localInserts;
|
||||
this.localUpdates = localUpdates;
|
||||
this.remoteInserts = remoteInserts;
|
||||
this.remoteUpdates = remoteUpdates;
|
||||
this.remoteDeletes = remoteDeletes;
|
||||
}
|
||||
}
|
||||
|
||||
interface ConflictMerger<E extends SignalRecord> {
|
||||
@NonNull Optional<E> getMatching(@NonNull E record);
|
||||
@NonNull Collection<E> getInvalidEntries(@NonNull Collection<E> remoteRecords);
|
||||
@NonNull E merge(@NonNull E remote, @NonNull E local, @NonNull StorageKeyGenerator keyGenerator);
|
||||
}
|
||||
|
||||
private static final class MultipleExistingAccountsException extends IllegalArgumentException {}
|
||||
private static final class InvalidAccountInsertException extends IllegalArgumentException {}
|
||||
private static final class InvalidAccountUpdateException extends IllegalArgumentException {}
|
||||
|
||||
@@ -75,7 +75,6 @@ public final class FeatureFlags {
|
||||
private static final String ANIMATED_STICKER_MIN_TOTAL_MEMORY = "android.animatedStickerMinTotalMemory";
|
||||
private static final String MESSAGE_PROCESSOR_ALARM_INTERVAL = "android.messageProcessor.alarmIntervalMins";
|
||||
private static final String MESSAGE_PROCESSOR_DELAY = "android.messageProcessor.foregroundDelayMs";
|
||||
private static final String STORAGE_SYNC_V2 = "android.storageSyncV2.3";
|
||||
private static final String NOTIFICATION_REWRITE = "android.notificationRewrite";
|
||||
private static final String MP4_GIF_SEND_SUPPORT = "android.mp4GifSendSupport";
|
||||
|
||||
@@ -109,7 +108,6 @@ public final class FeatureFlags {
|
||||
ANIMATED_STICKER_MIN_TOTAL_MEMORY,
|
||||
MESSAGE_PROCESSOR_ALARM_INTERVAL,
|
||||
MESSAGE_PROCESSOR_DELAY,
|
||||
STORAGE_SYNC_V2,
|
||||
NOTIFICATION_REWRITE,
|
||||
MP4_GIF_SEND_SUPPORT
|
||||
);
|
||||
@@ -155,7 +153,6 @@ public final class FeatureFlags {
|
||||
MESSAGE_PROCESSOR_ALARM_INTERVAL,
|
||||
MESSAGE_PROCESSOR_DELAY,
|
||||
GV1_FORCED_MIGRATE,
|
||||
STORAGE_SYNC_V2,
|
||||
NOTIFICATION_REWRITE,
|
||||
MP4_GIF_SEND_SUPPORT
|
||||
);
|
||||
@@ -344,11 +341,6 @@ public final class FeatureFlags {
|
||||
return getInteger(ANIMATED_STICKER_MIN_TOTAL_MEMORY, (int) ByteUnit.GIGABYTES.toMegabytes(3));
|
||||
}
|
||||
|
||||
/** Whether or not to use {@link org.thoughtcrime.securesms.jobs.StorageSyncJobV2}. */
|
||||
public static boolean storageSyncV2() {
|
||||
return getBoolean(STORAGE_SYNC_V2, true);
|
||||
}
|
||||
|
||||
/** Whether or not to use the new notification system. */
|
||||
public static boolean useNewNotificationSystem() {
|
||||
return getBoolean(NOTIFICATION_REWRITE, false) && Build.VERSION.SDK_INT >= 26;
|
||||
|
||||
Reference in New Issue
Block a user