Rewrite storage service change processing.

fork-5.53.8
Greyson Parrelli 2021-03-16 10:01:46 -04:00
rodzic 552b19cbb0
commit 0e200b1fb6
42 zmienionych plików z 1913 dodań i 208 usunięć

Wyświetl plik

@ -746,10 +746,6 @@
android:authorities="${applicationId}.database.conversation"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$ConversationList"
android:authorities="${applicationId}.database.conversationlist"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Attachment"
android:authorities="${applicationId}.database.attachment"
android:exported="false" />

Wyświetl plik

@ -194,7 +194,7 @@ public class DirectoryHelper {
if (newRegisteredState != originalRegisteredState) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob());
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
ApplicationDependencies.getJobManager().add(StorageSyncJob.create());
if (notifyOfNewUsers && newRegisteredState == RegisteredState.REGISTERED && recipient.resolve().isSystemContact()) {
notifyNewUsers(context, Collections.singletonList(recipient.getId()));

Wyświetl plik

@ -61,7 +61,6 @@ public abstract class Database {
protected void notifyConversationListListeners() {
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
}
protected void notifyStickerListeners() {
@ -87,11 +86,6 @@ public abstract class Database {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getVerboseUriForThread(threadId));
}
@Deprecated
protected void setNotifyConversationListListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI);
}
@Deprecated
protected void setNotifyStickerListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Sticker.CONTENT_URI);

Wyświetl plik

@ -16,13 +16,6 @@ import org.thoughtcrime.securesms.BuildConfig;
*/
public class DatabaseContentProviders {
public static class ConversationList extends NoopContentProvider {
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.conversationlist";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY;
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
}
public static class Conversation extends NoopContentProvider {
private static final String CONTENT_AUTHORITY = BuildConfig.APPLICATION_ID + ".database.conversation";
private static final String CONTENT_URI_STRING = "content://" + CONTENT_AUTHORITY + "/";

Wyświetl plik

@ -117,8 +117,6 @@ public final class DatabaseObserver {
listener.onChanged();
}
});
application.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
}
public void notifyPaymentListeners(@NonNull UUID paymentId) {

Wyświetl plik

@ -44,7 +44,7 @@ import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.RecordUpdate;
import org.thoughtcrime.securesms.storage.StorageRecordUpdate;
import org.thoughtcrime.securesms.storage.StorageSyncModels;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.Bitmask;
@ -61,12 +61,15 @@ import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
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.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.util.UuidUtil;
@ -812,12 +815,170 @@ public class RecipientDatabase extends Database {
}
}
public boolean applyStorageSyncUpdates(@NonNull Collection<SignalContactRecord> contactInserts,
@NonNull Collection<RecordUpdate<SignalContactRecord>> contactUpdates,
@NonNull Collection<SignalGroupV1Record> groupV1Inserts,
@NonNull Collection<RecordUpdate<SignalGroupV1Record>> groupV1Updates,
@NonNull Collection<SignalGroupV2Record> groupV2Inserts,
@NonNull Collection<RecordUpdate<SignalGroupV2Record>> groupV2Updates)
public void applyStorageSyncContactInsert(@NonNull SignalContactRecord insert) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
ContentValues values = getValuesForStorageContact(insert, true);
long id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE);
RecipientId recipientId = null;
if (id < 0) {
Log.w(TAG, "[applyStorageSyncContactInsert] Failed to insert. Possibly merging.");
recipientId = getAndPossiblyMerge(insert.getAddress().getUuid().get(), insert.getAddress().getNumber().get(), true);
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId));
} 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);
}
public void applyStorageSyncContactUpdate(@NonNull StorageRecordUpdate<SignalContactRecord> update) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
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, "[applyStorageSyncContactUpdate] Failed to update a user by storageId.");
RecipientId recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getOld().getId().getRaw())).get();
Log.w(TAG, "[applyStorageSyncContactUpdate] Found user " + recipientId + ". Possibly merging.");
recipientId = getAndPossiblyMerge(update.getNew().getAddress().getUuid().orNull(), update.getNew().getAddress().getNumber().orNull(), true);
Log.w(TAG, "[applyStorageSyncContactUpdate] 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);
db.update(TABLE_NAME, clearValues, ID_WHERE, SqlUtil.buildArgs(recipientId));
}
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);
}
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipientId, update.getNew());
Recipient.live(recipientId).refresh();
}
public void applyStorageSyncGroupV1Insert(@NonNull SignalGroupV1Record insert) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
long id = db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert));
RecipientId recipientId = RecipientId.from(id);
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipientId, insert);
Recipient.live(recipientId).refresh();
}
public void applyStorageSyncGroupV1Update(@NonNull StorageRecordUpdate<SignalGroupV1Record> update) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
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()));
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipient.getId(), update.getNew());
recipient.live().refresh();
}
public void applyStorageSyncGroupV2Insert(@NonNull SignalGroupV2Record insert) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
GroupMasterKey masterKey = insert.getMasterKeyOrThrow();
GroupId.V2 groupId = GroupId.v2(masterKey);
ContentValues values = getValuesForStorageGroupV2(insert);
long id = db.insertOrThrow(TABLE_NAME, null, values);
Recipient recipient = Recipient.externalGroupExact(context, 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));
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipient.getId(), insert);
recipient.live().refresh();
}
public void applyStorageSyncGroupV2Update(@NonNull StorageRecordUpdate<SignalGroupV2Record> update) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
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));
DatabaseFactory.getThreadDatabase(context).applyStorageSyncUpdate(recipient.getId(), update.getNew());
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);
@ -889,7 +1050,7 @@ public class RecipientDatabase extends Database {
needsRefresh.add(recipientId);
}
for (RecordUpdate<SignalContactRecord> update : contactUpdates) {
for (StorageRecordUpdate<SignalContactRecord> update : contactUpdates) {
ContentValues values = getValuesForStorageContact(update.getNew(), false);
try {
@ -932,7 +1093,7 @@ public class RecipientDatabase extends Database {
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true);
} else if ((newIdentityRecord.isPresent() && newIdentityRecord.get().getVerifiedStatus() != VerifiedStatus.VERIFIED) &&
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
(oldIdentityRecord.isPresent() && oldIdentityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED))
{
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true);
}
@ -958,7 +1119,7 @@ public class RecipientDatabase extends Database {
}
}
for (RecordUpdate<SignalGroupV1Record> update : groupV1Updates) {
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())});
@ -971,7 +1132,7 @@ public class RecipientDatabase extends Database {
threadDatabase.applyStorageSyncUpdate(recipient.getId(), update.getNew());
needsRefresh.add(recipient.getId());
}
for (SignalGroupV2Record insert : groupV2Inserts) {
GroupMasterKey masterKey = insert.getMasterKeyOrThrow();
GroupId.V2 groupId = GroupId.v2(masterKey);
@ -988,9 +1149,9 @@ public class RecipientDatabase extends Database {
Log.i(TAG, "Creating restore placeholder for " + groupId);
DatabaseFactory.getGroupDatabase(context)
.create(masterKey,
DecryptedGroup.newBuilder()
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
.build());
DecryptedGroup.newBuilder()
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
.build());
Log.i(TAG, "Scheduling request for latest group info for " + groupId);
@ -1000,7 +1161,7 @@ public class RecipientDatabase extends Database {
needsRefresh.add(recipient.getId());
}
for (RecordUpdate<SignalGroupV2Record> update : groupV2Updates) {
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())});
@ -2571,6 +2732,22 @@ public class RecipientDatabase extends Database {
}
}
public void clearDirtyStateForRecords(@NonNull List<SignalStorageRecord> records) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
Preconditions.checkArgument(db.inTransaction(), "Database should already be in a transaction.");
ContentValues values = new ContentValues();
values.put(DIRTY, DirtyState.CLEAN.getId());
String query = STORAGE_SERVICE_ID + " = ?";
for (SignalRecord record : records) {
String[] args = SqlUtil.buildArgs(Base64.encodeBytes(record.getId().getRaw()));
db.update(TABLE_NAME, values, query, args);
}
}
public void clearDirtyState(@NonNull List<RecipientId> recipients) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();

Wyświetl plik

@ -133,11 +133,7 @@ public class SearchDatabase extends Database {
return null;
}
Cursor cursor = db.rawQuery(MESSAGES_QUERY, new String[] { fullTextSearchQuery,
fullTextSearchQuery });
setNotifyConversationListListeners(cursor);
return cursor;
return db.rawQuery(MESSAGES_QUERY, new String[] { fullTextSearchQuery, fullTextSearchQuery });
}
public Cursor queryMessages(@NonNull String query, long threadId) {
@ -148,13 +144,10 @@ public class SearchDatabase extends Database {
return null;
}
Cursor cursor = db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { fullTextSearchQuery,
String.valueOf(threadId),
fullTextSearchQuery,
String.valueOf(threadId) });
setNotifyConversationListListeners(cursor);
return cursor;
return db.rawQuery(MESSAGES_FOR_THREAD_QUERY, new String[] { fullTextSearchQuery,
String.valueOf(threadId),
fullTextSearchQuery,
String.valueOf(threadId) });
}
private static String createFullTextSearchQuery(@NonNull String query) {

Wyświetl plik

@ -7,14 +7,20 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.util.guava.Preconditions;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.SignalStorage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
@ -79,26 +85,39 @@ public class StorageKeyDatabase extends Database {
db.beginTransaction();
try {
for (SignalStorageRecord insert : inserts) {
ContentValues values = new ContentValues();
values.put(TYPE, insert.getType());
values.put(STORAGE_ID, Base64.encodeBytes(insert.getId().getRaw()));
db.insert(TABLE_NAME, null, values);
}
String deleteQuery = STORAGE_ID + " = ?";
for (SignalStorageRecord delete : deletes) {
String[] args = new String[] { Base64.encodeBytes(delete.getId().getRaw()) };
db.delete(TABLE_NAME, deleteQuery, args);
}
insert(inserts);
delete(Stream.of(deletes).map(SignalStorageRecord::getId).toList());
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void insert(@NonNull Collection<SignalStorageRecord> inserts) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
Preconditions.checkArgument(db.inTransaction(), "Must be in a transaction!");
for (SignalStorageRecord insert : inserts) {
ContentValues values = new ContentValues();
values.put(TYPE, insert.getType());
values.put(STORAGE_ID, Base64.encodeBytes(insert.getId().getRaw()));
db.insert(TABLE_NAME, null, values);
}
}
public void delete(@NonNull Collection<StorageId> deletes) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
String deleteQuery = STORAGE_ID + " = ?";
Preconditions.checkArgument(db.inTransaction(), "Must be in a transaction!");
for (StorageId id : deletes) {
String[] args = SqlUtil.buildArgs(Base64.encodeBytes(id.getRaw()));
db.delete(TABLE_NAME, deleteQuery, args);
}
}
public void deleteByType(int type) {

Wyświetl plik

@ -514,7 +514,6 @@ public class ThreadDatabase extends Database {
}
Cursor cursor = cursors.size() > 1 ? new MergeCursor(cursors.toArray(new Cursor[cursors.size()])) : cursors.get(0);
setNotifyConversationListListeners(cursor);
return cursor;
}
@ -707,8 +706,6 @@ public class ThreadDatabase extends Database {
Cursor cursor = db.rawQuery(query, new String[]{});
setNotifyConversationListListeners(cursor);
return cursor;
}
@ -717,8 +714,6 @@ public class ThreadDatabase extends Database {
String query = createQuery(ARCHIVED + " = ? AND " + MESSAGE_COUNT + " != 0", offset, limit, false);
Cursor cursor = db.rawQuery(query, new String[]{archived});
setNotifyConversationListListeners(cursor);
return cursor;
}
@ -1225,12 +1220,13 @@ public class ThreadDatabase extends Database {
private void applyStorageSyncUpdate(@NonNull RecipientId recipientId, boolean archived, boolean forcedUnread) {
ContentValues values = new ContentValues();
values.put(ARCHIVED, archived);
values.put(ARCHIVED, archived ? 1 : 0);
Long threadId = getThreadIdFor(recipientId);
if (forcedUnread) {
values.put(READ, ReadStatus.FORCED_UNREAD.serialize());
} else {
Long threadId = getThreadIdFor(recipientId);
if (threadId != null) {
int unreadCount = DatabaseFactory.getMmsSmsDatabase(context).getUnreadCount(threadId);
@ -1240,6 +1236,10 @@ public class ThreadDatabase extends Database {
}
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(recipientId));
if (threadId != null) {
notifyConversationListeners(threadId);
}
}
public boolean update(long threadId, boolean unarchive) {

Wyświetl plik

@ -141,6 +141,7 @@ public final class JobManagerFactories {
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());
put(UpdateApkJob.KEY, new UpdateApkJob.Factory());

Wyświetl plik

@ -99,8 +99,7 @@ public class StorageAccountRestoreJob extends BaseJob {
Log.i(TAG, "Applying changes locally...");
StorageId selfStorageId = StorageId.forAccount(Recipient.self().getStorageServiceId());
StorageSyncHelper.applyAccountStorageSyncUpdates(context, selfStorageId, accountRecord, false);
StorageSyncHelper.applyAccountStorageSyncUpdates(context, Recipient.self(), accountRecord, false);
JobManager jobManager = ApplicationDependencies.getJobManager();

Wyświetl plik

@ -30,6 +30,7 @@ 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;
@ -70,7 +71,7 @@ public class StorageSyncJob extends BaseJob {
private static final String TAG = Log.tag(StorageSyncJob.class);
public StorageSyncJob() {
private StorageSyncJob() {
this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY)
.setQueue(QUEUE_KEY)
.setMaxInstancesForFactory(2)
@ -82,6 +83,22 @@ public class StorageSyncJob extends BaseJob {
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;

Wyświetl plik

@ -0,0 +1,435 @@
package org.thoughtcrime.securesms.jobs;
import android.content.Context;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import net.sqlcipher.database.SQLiteDatabase;
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.StorageKeyDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
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.AccountRecordProcessor;
import org.thoughtcrime.securesms.storage.ContactRecordProcessor;
import org.thoughtcrime.securesms.storage.GroupV1RecordProcessor;
import org.thoughtcrime.securesms.storage.GroupV2RecordProcessor;
import org.thoughtcrime.securesms.storage.StorageRecordProcessor;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult;
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.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.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
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.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
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.
*
* == Important Implementation Notes ==
*
* - We want to use a transaction to guarantee atomicity of our changes and to prevent other threads
* from writing while the sync is happening. But that means we also need to be very careful with
* what happens inside the transaction. Namely, we *cannot* perform network activity inside the
* transaction.
*
* - This puts us in a funny situation where we have to get remote data, begin a transaction to
* resolve the sync, and then end the transaction (and therefore commit our changes) *before*
* we write the data remotely. Normally, this would be dangerous, as our view of the data could
* fall out of sync if the network request fails. However, because of how the sync works, as long
* as we don't update our local manifest version until after the network request succeeds, it
* should all sort itself out in the retry. Because if our network request failed, then we
* wouldn't have written all of the new keys, and we'll still see a bunch of remote-only keys that
* we'll merge with local data to generate another equally-valid set of remote changes.
*
*
* == Technical Overview ==
*
* The Storage Service is, at it's core, a dumb key-value store. It stores various types of records,
* each of which is given an ID. It also stores a manifest, which has the complete list of all IDs.
* The manifest has a monotonically-increasing version associated with it. Whenever a change is
* made to the stored data, you upload a new manifest with the updated ID set.
*
* An ID corresponds to an unchanging snapshot of a record. That is, if the underlying record is
* updated, that update is performed by deleting the old ID/record and inserting a new one. This
* makes it easy to determine what's changed in a given version of a manifest -- simply diff the
* list of IDs in the manifest with the list of IDs we have locally.
*
* So, at it's core, syncing isn't all that complicated.
* - If we see the remote manifest version is newer than ours, then we grab the manifest and compute
* the diff in IDs.
* - Then, we fetch the actual records that correspond to the remote-only IDs.
* - Afterwards, we take those records and merge them into our local data store.
* - The merging process could result in changes that need to be written back to the service, so
* we write those back.
* - Finally, we look at any other local changes that were made (independent of the ID diff) and
* make sure those are written to the service.
*
* Of course, you'll notice that there's a lot of code to support that goal. That's mostly because
* converting local data into a format that can be compared with, merged, and eventually written
* back to both local and remote data stores is tiresome. There's also lots of general bookkeeping,
* error handling, cleanup scenarios, logging, etc.
*/
public class StorageSyncJobV2 extends BaseJob {
public static final String KEY = "StorageSyncJobV2";
public static final String QUEUE_KEY = "StorageSyncingJobs";
private static final String TAG = Log.tag(StorageSyncJobV2.class);
StorageSyncJobV2() {
this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY)
.setQueue(QUEUE_KEY)
.setMaxInstancesForFactory(2)
.setLifespan(TimeUnit.DAYS.toMillis(1))
.setMaxAttempts(3)
.build());
}
private StorageSyncJobV2(@NonNull Parameters parameters) {
super(parameters);
}
@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 {
Recipient self = Recipient.self();
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(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 Sync] Newer manifest version found!");
List<StorageId> localStorageIdsBeforeMerge = getAllLocalStorageIds(context, Recipient.self().fresh());
KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageIds(), localStorageIdsBeforeMerge);
if (keyDifference.hasTypeMismatches()) {
Log.w(TAG, "[Remote Sync] Found type mismatches in the key sets! Scheduling a force push after this sync completes.");
needsForcePush = true;
}
if (!keyDifference.isEmpty()) {
Log.i(TAG, "[Remote Sync] Retrieving records for key difference: " + keyDifference);
List<SignalStorageRecord> remoteOnly = accountManager.readStorageRecords(storageServiceKey, keyDifference.getRemoteOnlyKeys());
List<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, self, keyDifference.getLocalOnlyKeys());
if (remoteOnly.size() != keyDifference.getRemoteOnlyKeys().size()) {
Log.w(TAG, "[Remote Sync] Could not find all remote-only records! Requested: " + keyDifference.getRemoteOnlyKeys().size() + ", Found: " + remoteOnly.size() + ". Scheduling a force push after this sync completes.");
needsForcePush = true;
}
List<SignalContactRecord> remoteContacts = new LinkedList<>();
List<SignalGroupV1Record> remoteGv1 = new LinkedList<>();
List<SignalGroupV2Record> remoteGv2 = new LinkedList<>();
List<SignalAccountRecord> remoteAccount = new LinkedList<>();
List<SignalStorageRecord> remoteUnknown = new LinkedList<>();
for (SignalStorageRecord remote : remoteOnly) {
if (remote.getContact().isPresent()) {
remoteContacts.add(remote.getContact().get());
} else if (remote.getGroupV1().isPresent()) {
remoteGv1.add(remote.getGroupV1().get());
} else if (remote.getGroupV2().isPresent()) {
remoteGv2.add(remote.getGroupV2().get());
} else if (remote.getAccount().isPresent()) {
remoteAccount.add(remote.getAccount().get());
} else {
remoteUnknown.add(remote);
}
}
WriteOperationResult mergeWriteOperation;
SQLiteDatabase db = DatabaseFactory.getInstance(context).getRawDatabase();
db.beginTransaction();
try {
StorageRecordProcessor.Result<SignalContactRecord> contactResult = new ContactRecordProcessor(context, self).process(remoteContacts, StorageSyncHelper.KEY_GENERATOR);
StorageRecordProcessor.Result<SignalGroupV1Record> gv1Result = new GroupV1RecordProcessor(context).process(remoteGv1, StorageSyncHelper.KEY_GENERATOR);
StorageRecordProcessor.Result<SignalGroupV2Record> gv2Result = new GroupV2RecordProcessor(context).process(remoteGv2, StorageSyncHelper.KEY_GENERATOR);
StorageRecordProcessor.Result<SignalAccountRecord> accountResult = new AccountRecordProcessor(context, self).process(remoteAccount, StorageSyncHelper.KEY_GENERATOR);
List<SignalStorageRecord> unknownInserts = remoteUnknown;
List<StorageId> unknownDeletes = Stream.of(keyDifference.getLocalOnlyKeys()).filter(StorageId::isUnknown).toList();
storageKeyDatabase.insert(unknownInserts);
storageKeyDatabase.delete(unknownDeletes);
List<StorageId> localStorageIdsAfterMerge = getAllLocalStorageIds(context, Recipient.self().fresh());
if (contactResult.isLocalOnly() && gv1Result.isLocalOnly() && gv2Result.isLocalOnly() && accountResult.isLocalOnly() && unknownInserts.isEmpty() && unknownDeletes.isEmpty()) {
Log.i(TAG, "Result: No remote updates/deletes");
Log.i(TAG, "IDs : " + localStorageIdsBeforeMerge.size() + " IDs before merge, " + localStorageIdsAfterMerge.size() + " IDs after merge");
} else {
Log.i(TAG, "Contacts: " + contactResult.toString());
Log.i(TAG, "GV1 : " + gv1Result.toString());
Log.i(TAG, "GV2 : " + gv2Result.toString());
Log.i(TAG, "Account : " + accountResult.toString());
Log.i(TAG, "Unknowns: " + unknownInserts.size() + " Inserts, " + unknownDeletes.size() + " Deletes");
Log.i(TAG, "IDs : " + localStorageIdsBeforeMerge.size() + " IDs before merge, " + localStorageIdsAfterMerge.size() + " IDs after merge");
}
localOnly.removeAll(contactResult.getLocalMatches());
localOnly.removeAll(gv1Result.getLocalMatches());
localOnly.removeAll(gv2Result.getLocalMatches());
localOnly.removeAll(accountResult.getLocalMatches());
recipientDatabase.clearDirtyStateForRecords(localOnly);
Log.i(TAG, "[Remote Sync] After the conflict resolution, there are " + localOnly.size() + " local-only records remaining.");
//noinspection unchecked Stop yelling at my beautiful method signatures
mergeWriteOperation = StorageSyncHelper.createWriteOperation(remoteManifest.get().getVersion(), localStorageIdsAfterMerge, localOnly, contactResult, gv1Result, gv2Result, accountResult);
StorageSyncValidations.validate(mergeWriteOperation);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
}
if (!mergeWriteOperation.isEmpty()) {
Log.i(TAG, "[Remote Sync] WriteOperationResult :: " + mergeWriteOperation);
Log.i(TAG, "[Remote Sync] We have something to write remotely.");
if (mergeWriteOperation.getManifest().getStorageIds().size() != remoteManifest.get().getStorageIds().size() + mergeWriteOperation.getInserts().size() - mergeWriteOperation.getDeletes().size()) {
Log.w(TAG, String.format(Locale.US, "[Remote Sync] Bad storage key management! originalRemoteKeys: %d, newRemoteKeys: %d, insertedKeys: %d, deletedKeys: %d",
remoteManifest.get().getStorageIds().size(), mergeWriteOperation.getManifest().getStorageIds().size(), mergeWriteOperation.getInserts().size(), mergeWriteOperation.getDeletes().size()));
}
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, mergeWriteOperation.getManifest(), mergeWriteOperation.getInserts(), mergeWriteOperation.getDeletes());
if (conflict.isPresent()) {
Log.w(TAG, "[Remote Sync] Hit a conflict when trying to resolve the conflict! Retrying.");
throw new RetryLaterException();
}
remoteManifestVersion = mergeWriteOperation.getManifest().getVersion();
needsMultiDeviceSync = true;
} else {
Log.i(TAG, "[Remote Sync] No remote writes needed.");
}
Log.i(TAG, "[Remote Sync] Updating local manifest version to: " + remoteManifestVersion);
TextSecurePreferences.setStorageManifestVersion(context, remoteManifestVersion);
} else {
Log.i(TAG, "[Remote Sync] Remote version was newer, there were no remote-only keys.");
Log.i(TAG, "[Remote Sync] Updating local manifest version to: " + remoteManifest.get().getVersion());
TextSecurePreferences.setStorageManifestVersion(context, remoteManifest.get().getVersion());
}
}
localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context);
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);
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.updateStorageKeys(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;
}
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.getStorageKeyDatabase(context).getAllKeys());
}
private static @NonNull List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull Recipient self, @NonNull List<StorageId> ids) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(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<StorageSyncJobV2> {
@Override
public @NonNull StorageSyncJobV2 create(@NonNull Parameters parameters, @NonNull Data data) {
return new StorageSyncJobV2(parameters);
}
}
}

Wyświetl plik

@ -41,12 +41,12 @@ public class StorageServiceMigrationJob extends MigrationJob {
if (TextSecurePreferences.isMultiDevice(context)) {
Log.i(TAG, "Multi-device.");
jobManager.startChain(new StorageSyncJob())
jobManager.startChain(StorageSyncJob.create())
.then(new MultiDeviceKeysUpdateJob())
.enqueue();
} else {
Log.i(TAG, "Single-device.");
jobManager.add(new StorageSyncJob());
jobManager.add(StorageSyncJob.create());
}
}

Wyświetl plik

@ -83,7 +83,7 @@ public class PinRestoreRepository {
ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN);
stopwatch.split("AccountRestore");
ApplicationDependencies.getJobManager().runSynchronously(new StorageSyncJob(), TimeUnit.SECONDS.toMillis(10));
ApplicationDependencies.getJobManager().runSynchronously(StorageSyncJob.create(), TimeUnit.SECONDS.toMillis(10));
stopwatch.split("ContactRestore");
stopwatch.stop(TAG);

Wyświetl plik

@ -31,7 +31,7 @@ public final class RegistrationUtil {
{
Log.i(TAG, "Marking registration completed.", new Throwable());
SignalStore.registrationValues().setRegistrationComplete();
ApplicationDependencies.getJobManager().startChain(new StorageSyncJob())
ApplicationDependencies.getJobManager().startChain(StorageSyncJob.create())
.then(new DirectoryRefreshJob(false))
.enqueue();
} else if (!SignalStore.registrationValues().isRegistrationComplete()) {

Wyświetl plik

@ -46,7 +46,7 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger<SignalAc
}
@Override
public @NonNull SignalAccountRecord merge(@NonNull SignalAccountRecord remote, @NonNull SignalAccountRecord local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
public @NonNull SignalAccountRecord merge(@NonNull SignalAccountRecord remote, @NonNull SignalAccountRecord local, @NonNull StorageKeyGenerator keyGenerator) {
String givenName;
String familyName;

Wyświetl plik

@ -0,0 +1,177 @@
package org.thoughtcrime.securesms.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
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.api.storage.StorageId;
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;
/**
* Processes {@link SignalAccountRecord}s. Unlike some other {@link StorageRecordProcessor}s, this
* one has some statefulness in order to reject all but one account record (since we should have
* exactly one account record).
*/
public class AccountRecordProcessor extends DefaultStorageRecordProcessor<SignalAccountRecord> {
private static final String TAG = Log.tag(AccountRecordProcessor.class);
private final Context context;
private final RecipientDatabase recipientDatabase;
private final SignalAccountRecord localAccountRecord;
private final Recipient self;
private boolean foundAccountRecord = false;
public AccountRecordProcessor(@NonNull Context context, @NonNull Recipient self) {
this(context, self, StorageSyncHelper.buildAccountRecord(context, self).getAccount().get(), DatabaseFactory.getRecipientDatabase(context));
}
AccountRecordProcessor(@NonNull Context context, @NonNull Recipient self, @NonNull SignalAccountRecord localAccountRecord, @NonNull RecipientDatabase recipientDatabase) {
this.context = context;
this.self = self;
this.recipientDatabase = recipientDatabase;
this.localAccountRecord = localAccountRecord;
}
/**
* We want to catch:
* - Multiple account records
*/
@Override
boolean isInvalid(@NonNull SignalAccountRecord remote) {
if (foundAccountRecord) {
Log.w(TAG, "Found an additional account record! Considering it invalid.");
return true;
}
foundAccountRecord = true;
return false;
}
@Override
public @NonNull Optional<SignalAccountRecord> getMatching(@NonNull SignalAccountRecord record) {
return Optional.of(localAccountRecord);
}
@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 matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars);
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars);
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)
.build();
}
}
@Override
void insertLocal(@NonNull SignalAccountRecord record) {
throw new UnsupportedOperationException("We should always have a local AccountRecord, so we should never been inserting a new one.");
}
@Override
void updateLocal(@NonNull StorageRecordUpdate<SignalAccountRecord> update) {
Log.i(TAG, "Local account update: " + update.toString());
StorageSyncHelper.applyAccountStorageSyncUpdates(context, self, update.getNew(), true);
}
@Override
public int compare(@NonNull SignalAccountRecord lhs, @NonNull SignalAccountRecord rhs) {
return 0;
}
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)
{
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);
}
}

Wyświetl plik

@ -96,7 +96,7 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
}
@Override
public @NonNull SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
public @NonNull SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageKeyGenerator keyGenerator) {
String givenName;
String familyName;

Wyświetl plik

@ -0,0 +1,172 @@
package org.thoughtcrime.securesms.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
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.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
public class ContactRecordProcessor extends DefaultStorageRecordProcessor<SignalContactRecord> {
private static final String TAG = Log.tag(ContactRecordProcessor.class);
private final Recipient self;
private final RecipientDatabase recipientDatabase;
public ContactRecordProcessor(@NonNull Context context, @NonNull Recipient self) {
this(self, DatabaseFactory.getRecipientDatabase(context));
}
ContactRecordProcessor(@NonNull Recipient self, @NonNull RecipientDatabase recipientDatabase) {
this.self = self;
this.recipientDatabase = recipientDatabase;
}
/**
* Error cases:
* - You can't have a contact record without an address component.
* - You can't have a contact record for yourself. That should be an account record.
*
* Note: This method could be written more succinctly, but the logs are useful :)
*/
@Override
boolean isInvalid(@NonNull SignalContactRecord remote) {
SignalServiceAddress address = remote.getAddress();
if (address == null) {
Log.w(TAG, "No address on the ContentRecord -- marking as invalid.");
return true;
} else if ((self.getUuid().isPresent() && address.getUuid().equals(self.getUuid())) ||
(self.getE164().isPresent() && address.getNumber().equals(self.getE164())))
{
Log.w(TAG, "Found a ContactRecord for ourselves -- marking as invalid.");
return true;
} else {
return false;
}
}
@Override
@NonNull Optional<SignalContactRecord> getMatching(@NonNull SignalContactRecord remote) {
SignalServiceAddress address = remote.getAddress();
Optional<RecipientId> byUuid = address.getUuid().isPresent() ? recipientDatabase.getByUuid(address.getUuid().get()) : Optional.absent();
Optional<RecipientId> byE164 = address.getNumber().isPresent() ? recipientDatabase.getByE164(address.getNumber().get()) : Optional.absent();
return byUuid.or(byE164).transform(recipientDatabase::getRecipientSettingsForSync)
.transform(StorageSyncModels::localToRemoteRecord)
.transform(r -> r.getContact().get());
}
@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();
}
}
@Override
void insertLocal(@NonNull SignalContactRecord record) {
recipientDatabase.applyStorageSyncContactInsert(record);
}
@Override
void updateLocal(@NonNull StorageRecordUpdate<SignalContactRecord> update) {
Log.i(TAG, "Local contact update: " + update.toString());
recipientDatabase.applyStorageSyncContactUpdate(update);
}
@Override
public int compare(@NonNull SignalContactRecord lhs, @NonNull SignalContactRecord rhs) {
if (Objects.equals(lhs.getAddress().getUuid(), rhs.getAddress().getUuid()) ||
Objects.equals(lhs.getAddress().getNumber(), rhs.getAddress().getNumber()))
{
return 0;
} else {
return lhs.getAddress().getIdentifier().compareTo(rhs.getAddress().getIdentifier());
}
}
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;
}
}

Wyświetl plik

@ -0,0 +1,99 @@
package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.SignalRecord;
import java.io.IOException;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* An implementation of {@link StorageRecordProcessor} that solidifies a pattern and reduces
* duplicate code in individual implementations.
*
* Concerning the implementation of {@link #compare(Object, Object)}, it's purpose is to detect if
* two items would map to the same logical entity (i.e. they would correspond to the same record in
* our local store). We use it for a {@link TreeSet}, so mainly it's just important that the '0'
* case is correct. Other cases are whatever, just make it something stable.
*/
abstract class DefaultStorageRecordProcessor<E extends SignalRecord> implements StorageRecordProcessor<E>, Comparator<E> {
private static final String TAG = Log.tag(DefaultStorageRecordProcessor.class);
/**
* One type of invalid remote data this handles is two records mapping to the same local data. We
* have to trim this bad data out, because if we don't, we'll upload an ID set that only has one
* of the IDs in it, but won't properly delete the dupes, which will then fail our validation
* checks.
*
* This is a bit tricky -- as we process records, ID's are written back to the local store, so we
* can't easily be like "oh multiple records are mapping to the same local storage ID". And in
* general we rely on SignalRecords to implement an equals() that includes the StorageId, so using
* a regular set is out. Instead, we use a {@link TreeSet}, which allows us to define a custom
* comparator for checking equality. Then we delegate to the subclass to tell us if two items are
* the same based on their actual data (i.e. two contacts having the same UUID, or two groups
* having the same MasterKey).
*/
@Override
public @NonNull Result<E> process(@NonNull Collection<E> remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException {
List<E> remoteDeletes = new LinkedList<>();
List<StorageRecordUpdate<E>> remoteUpdates = new LinkedList<>();
List<E> localMatches = new LinkedList<>();
Set<E> matchedRecords = new TreeSet<>(this);
for (E remote : remoteRecords) {
if (isInvalid(remote)) {
remoteDeletes.add(remote);
} else {
Optional<E> local = getMatching(remote);
if (local.isPresent()) {
E merged = merge(remote, local.get(), keyGenerator);
localMatches.add(local.get());
if (matchedRecords.contains(local.get())) {
Log.w(TAG, "Multiple remote records map to the same local record! Marking this one for deletion. (Type: " + local.get().getClass().getSimpleName() + ")");
remoteDeletes.add(remote);
} else {
matchedRecords.add(local.get());
if (!merged.equals(remote)) {
remoteUpdates.add(new StorageRecordUpdate<>(remote, merged));
}
if (!merged.equals(local.get())) {
updateLocal(new StorageRecordUpdate<>(local.get(), merged));
}
}
} else {
insertLocal(remote);
}
}
}
return new Result<>(remoteUpdates, remoteDeletes, localMatches);
}
/**
* @return True if the record is invalid and should be removed from storage service, otherwise false.
*/
abstract boolean isInvalid(@NonNull E remote);
/**
* Only records that pass the validity check (i.e. return false from {@link #isInvalid(SignalRecord)}
* make it to here, so you can assume all records are valid.
*/
abstract @NonNull Optional<E> getMatching(@NonNull E remote);
abstract @NonNull E merge(@NonNull E remote, @NonNull E local, @NonNull StorageKeyGenerator keyGenerator);
abstract void insertLocal(@NonNull E record) throws IOException;
abstract void updateLocal(@NonNull StorageRecordUpdate<E> update);
}

Wyświetl plik

@ -44,7 +44,7 @@ final class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger<Si
}
@Override
public @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
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();

Wyświetl plik

@ -0,0 +1,120 @@
package org.thoughtcrime.securesms.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.groups.BadGroupIdException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import java.util.Arrays;
/**
* Handles merging remote storage updates into local group v1 state.
*/
public final class GroupV1RecordProcessor extends DefaultStorageRecordProcessor<SignalGroupV1Record> {
private static final String TAG = Log.tag(GroupV1RecordProcessor.class);
private final GroupDatabase groupDatabase;
private final RecipientDatabase recipientDatabase;
public GroupV1RecordProcessor(@NonNull Context context) {
this(DatabaseFactory.getGroupDatabase(context), DatabaseFactory.getRecipientDatabase(context));
}
GroupV1RecordProcessor(@NonNull GroupDatabase groupDatabase, @NonNull RecipientDatabase recipientDatabase) {
this.groupDatabase = groupDatabase;
this.recipientDatabase = recipientDatabase;
}
/**
* We want to catch:
* - Invalid group ID's
* - GV1 ID's that map to GV2 ID's, meaning we've already migrated them.
*
* Note: This method could be written more succinctly, but the logs are useful :)
*/
@Override
boolean isInvalid(@NonNull SignalGroupV1Record remote) {
try {
GroupId.V1 id = GroupId.v1(remote.getGroupId());
Optional<GroupDatabase.GroupRecord> v2Record = groupDatabase.getGroup(id.deriveV2MigrationGroupId());
if (v2Record.isPresent()) {
Log.w(TAG, "We already have an upgraded V2 group for this V1 group -- marking as invalid.");
return true;
} else {
return false;
}
} catch (BadGroupIdException e) {
Log.w(TAG, "Bad Group ID -- marking as invalid.");
return true;
}
}
@Override
@NonNull Optional<SignalGroupV1Record> getMatching(@NonNull SignalGroupV1Record record) {
GroupId.V1 groupId = GroupId.v1orThrow(record.getGroupId());
Optional<RecipientId> recipientId = recipientDatabase.getByGroupId(groupId);
return recipientId.transform(recipientDatabase::getRecipientSettingsForSync)
.transform(StorageSyncModels::localToRemoteRecord)
.transform(r -> r.getGroupV1().get());
}
@Override
@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();
}
}
@Override
void insertLocal(@NonNull SignalGroupV1Record record) {
recipientDatabase.applyStorageSyncGroupV1Insert(record);
}
@Override
void updateLocal(@NonNull StorageRecordUpdate<SignalGroupV1Record> update) {
Log.i(TAG, "Local GV1 update: " + update.toString());
recipientDatabase.applyStorageSyncGroupV1Update(update);
}
@Override
public int compare(@NonNull SignalGroupV1Record lhs, @NonNull SignalGroupV1Record rhs) {
if (Arrays.equals(lhs.getGroupId(), rhs.getGroupId())) {
return 0;
} else {
return lhs.getGroupId()[0] - rhs.getGroupId()[0];
}
}
}

Wyświetl plik

@ -35,7 +35,7 @@ final class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger<Si
}
@Override
public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
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();

Wyświetl plik

@ -0,0 +1,119 @@
package org.thoughtcrime.securesms.storage;
import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ParcelUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
public final class GroupV2RecordProcessor extends DefaultStorageRecordProcessor<SignalGroupV2Record> {
private static final String TAG = Log.tag(GroupV2RecordProcessor.class);
private final Context context;
private final RecipientDatabase recipientDatabase;
private final Map<GroupId.V2, GroupId.V1> gv1GroupsByExpectedGv2Id;
public GroupV2RecordProcessor(@NonNull Context context) {
this(context, DatabaseFactory.getRecipientDatabase(context), DatabaseFactory.getGroupDatabase(context));
}
GroupV2RecordProcessor(@NonNull Context context, @NonNull RecipientDatabase recipientDatabase, @NonNull GroupDatabase groupDatabase) {
this.context = context;
this.recipientDatabase = recipientDatabase;
this.gv1GroupsByExpectedGv2Id = groupDatabase.getAllExpectedV2Ids();
}
@Override
boolean isInvalid(@NonNull SignalGroupV2Record remote) {
return remote.getMasterKeyBytes().length != GroupMasterKey.SIZE;
}
@Override
@NonNull Optional<SignalGroupV2Record> getMatching(@NonNull SignalGroupV2Record record) {
GroupId.V2 groupId = GroupId.v2(record.getMasterKeyOrThrow());
Optional<RecipientId> recipientId = recipientDatabase.getByGroupId(groupId);
return recipientId.transform(recipientDatabase::getRecipientSettingsForSync)
.transform(StorageSyncModels::localToRemoteRecord)
.transform(r -> r.getGroupV2().get());
}
@Override
@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();
}
}
/**
* This contains a pretty big compromise: In the event that the new GV2 group we learned about
* was, in fact, a migrated V1 group we already knew about, we handle the migration here. This
* isn't great because the migration will likely result in network activity. And because this is
* all happening in a transaction, this could keep the transaction open for longer than we'd like.
* However, given that nearly all V1 groups have already been migrated, we're at a point where
* this event should be extraordinarily rare, and it didn't seem worth it to add a lot of
* complexity to accommodate this specific scenario.
*/
@Override
void insertLocal(@NonNull SignalGroupV2Record record) throws IOException {
GroupId.V2 actualV2Id = GroupId.v2(record.getMasterKeyOrThrow());
GroupId.V1 possibleV1Id = gv1GroupsByExpectedGv2Id.get(actualV2Id);
if (possibleV1Id != null) {
Log.i(TAG, "Discovered a new GV2 ID that is actually a migrated V1 group! Migrating now.");
GroupsV1MigrationUtil.performLocalMigration(context, possibleV1Id);
} else {
recipientDatabase.applyStorageSyncGroupV2Insert(record);
}
}
@Override
void updateLocal(@NonNull StorageRecordUpdate<SignalGroupV2Record> update) {
Log.i(TAG, "Local GV2 update: " + update.toString());
recipientDatabase.applyStorageSyncGroupV2Update(update);
}
@Override
public int compare(@NonNull SignalGroupV2Record lhs, @NonNull SignalGroupV2Record rhs) {
if (Arrays.equals(lhs.getMasterKeyBytes(), rhs.getMasterKeyBytes())) {
return 0;
} else {
return lhs.getMasterKeyBytes()[0] - rhs.getMasterKeyBytes()[0];
}
}
}

Wyświetl plik

@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull;
/**
* Generates a key for use with the storage service.
*/
interface StorageKeyGenerator {
@NonNull
byte[] generate();
}

Wyświetl plik

@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.whispersystems.signalservice.api.storage.SignalRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import java.io.IOException;
import java.util.Collection;
/**
* Handles processing a remote record, which involves:
* - Applying an local changes that need to be made base don the remote record
* - Returning a result with any remote updates/deletes that need to be applied after merging with
* the local record.
*/
public interface StorageRecordProcessor<E extends SignalRecord> {
@NonNull Result<E> process(@NonNull Collection<E> remoteRecords, @NonNull StorageKeyGenerator keyGenerator) throws IOException;
final class Result<E extends SignalRecord> {
private final Collection<StorageRecordUpdate<E>> remoteUpdates;
private final Collection<E> remoteDeletes;
private final Collection<SignalStorageRecord> localMatches;
Result(@NonNull Collection<StorageRecordUpdate<E>> remoteUpdates, @NonNull Collection<E> remoteDeletes, @NonNull Collection<E> localMatches) {
this.remoteDeletes = remoteDeletes;
this.remoteUpdates = remoteUpdates;
this.localMatches = Stream.of(localMatches).map(SignalRecord::asStorageRecord).toList();
}
public @NonNull Collection<E> getRemoteDeletes() {
return remoteDeletes;
}
public @NonNull Collection<StorageRecordUpdate<E>> getRemoteUpdates() {
return remoteUpdates;
}
public @NonNull Collection<SignalStorageRecord> getLocalMatches() {
return localMatches;
}
public boolean isLocalOnly() {
return remoteUpdates.isEmpty() && remoteDeletes.isEmpty();
}
@Override
public @NonNull String toString() {
if (isLocalOnly()) {
return "Empty";
}
StringBuilder builder = new StringBuilder();
builder.append(remoteDeletes.size()).append(" Deletes, ").append(remoteUpdates.size()).append(" Updates\n");
for (StorageRecordUpdate<E> update : remoteUpdates) {
builder.append("- ").append(update.toString()).append("\n");
}
return super.toString();
}
}
}

Wyświetl plik

@ -0,0 +1,47 @@
package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull;
import org.whispersystems.signalservice.api.storage.SignalRecord;
import java.util.Objects;
/**
* Represents a pair of records: one old, and one new. The new record should replace the old.
*/
public class StorageRecordUpdate<E extends SignalRecord> {
private final E oldRecord;
private final E newRecord;
StorageRecordUpdate(@NonNull E oldRecord, @NonNull E newRecord) {
this.oldRecord = oldRecord;
this.newRecord = newRecord;
}
public @NonNull E getOld() {
return oldRecord;
}
public @NonNull E getNew() {
return newRecord;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StorageRecordUpdate that = (StorageRecordUpdate) o;
return oldRecord.equals(that.oldRecord) &&
newRecord.equals(that.newRecord);
}
@Override
public int hashCode() {
return Objects.hash(oldRecord, newRecord);
}
@Override
public @NonNull String toString() {
return newRecord.describeDiff(oldRecord);
}
}

Wyświetl plik

@ -54,9 +54,9 @@ public final class StorageSyncHelper {
private static final String TAG = Log.tag(StorageSyncHelper.class);
private static final KeyGenerator KEY_GENERATOR = () -> Util.getSecretBytes(16);
public static final StorageKeyGenerator KEY_GENERATOR = () -> Util.getSecretBytes(16);
private static KeyGenerator keyGenerator = KEY_GENERATOR;
private static StorageKeyGenerator keyGenerator = KEY_GENERATOR;
private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(2);
@ -231,6 +231,7 @@ public final class StorageSyncHelper {
remoteOnlyRawIds.remove(rawId);
localOnlyRawIds.remove(rawId);
hasTypeMismatch = true;
Log.w(TAG, "Remote type " + remote.getType() + " did not match local type " + local.getType() + "!");
}
}
@ -287,18 +288,18 @@ public final class StorageSyncHelper {
remoteInserts.addAll(Stream.of(groupV2MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV2).toList());
remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList());
Set<RecordUpdate<SignalStorageRecord>> remoteUpdates = new HashSet<>();
Set<StorageRecordUpdate<SignalStorageRecord>> remoteUpdates = new HashSet<>();
remoteUpdates.addAll(Stream.of(contactMergeResult.remoteUpdates)
.map(c -> new RecordUpdate<>(SignalStorageRecord.forContact(c.getOld()), SignalStorageRecord.forContact(c.getNew())))
.map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forContact(c.getOld()), SignalStorageRecord.forContact(c.getNew())))
.toList());
remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates)
.map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew())))
.map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew())))
.toList());
remoteUpdates.addAll(Stream.of(groupV2MergeResult.remoteUpdates)
.map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV2(c.getOld()), SignalStorageRecord.forGroupV2(c.getNew())))
.map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forGroupV2(c.getOld()), SignalStorageRecord.forGroupV2(c.getNew())))
.toList());
remoteUpdates.addAll(Stream.of(accountMergeResult.remoteUpdates)
.map(c -> new RecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew())))
.map(c -> new StorageRecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew())))
.toList());
Set<SignalRecord> remoteDeletes = new HashSet<>();
@ -331,11 +332,11 @@ public final class StorageSyncHelper {
{
List<SignalStorageRecord> inserts = new ArrayList<>();
inserts.addAll(mergeResult.getRemoteInserts());
inserts.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getNew).toList());
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(RecordUpdate::getOld).map(SignalStorageRecord::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());
@ -348,12 +349,37 @@ public final class StorageSyncHelper {
return new WriteOperationResult(manifest, inserts, Stream.of(deletes).map(StorageId::getRaw).toList());
}
/**
* Assumes all changes have already been applied to local data. That means that keys will be
* taken as-is, and the rest of the arguments are used to form the insert/delete sets.
*/
public static @NonNull WriteOperationResult createWriteOperation(long currentManifestVersion,
@NonNull List<StorageId> allStorageKeys,
@NonNull List<SignalStorageRecord> localOnlyRecords,
@NonNull StorageRecordProcessor.Result<? extends SignalRecord>... results)
{
Set<SignalStorageRecord> inserts = new LinkedHashSet<>(localOnlyRecords);
Set<StorageId> deletes = new LinkedHashSet<>();
for (StorageRecordProcessor.Result<? extends SignalRecord> result : results) {
for (StorageRecordUpdate<? extends SignalRecord> update : result.getRemoteUpdates()) {
inserts.add(update.getNew().asStorageRecord());
deletes.add(update.getOld().getId());
}
deletes.addAll(Stream.of(result.getRemoteDeletes()).map(SignalRecord::getId).toList());
}
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, new ArrayList<>(allStorageKeys));
return new WriteOperationResult(manifest, new ArrayList<>(inserts), Stream.of(deletes).map(StorageId::getRaw).toList());
}
public static @NonNull byte[] generateKey() {
return keyGenerator.generate();
}
@VisibleForTesting
static void setTestKeyGenerator(@Nullable KeyGenerator testKeyGenerator) {
static void setTestKeyGenerator(@Nullable StorageKeyGenerator testKeyGenerator) {
keyGenerator = testKeyGenerator != null ? testKeyGenerator : KEY_GENERATOR;
}
@ -363,8 +389,8 @@ public final class StorageSyncHelper {
{
Set<E> localInserts = new HashSet<>(remoteOnlyRecords);
Set<E> remoteInserts = new HashSet<>(localOnlyRecords);
Set<RecordUpdate<E>> localUpdates = new HashSet<>();
Set<RecordUpdate<E>> remoteUpdates = new HashSet<>();
Set<StorageRecordUpdate<E>> localUpdates = new HashSet<>();
Set<StorageRecordUpdate<E>> remoteUpdates = new HashSet<>();
Set<E> remoteDeletes = new HashSet<>(merger.getInvalidEntries(remoteOnlyRecords));
remoteOnlyRecords.removeAll(remoteDeletes);
@ -377,11 +403,11 @@ public final class StorageSyncHelper {
E merged = merger.merge(remote, local.get(), keyGenerator);
if (!merged.equals(remote)) {
remoteUpdates.add(new RecordUpdate<>(remote, merged));
remoteUpdates.add(new StorageRecordUpdate<>(remote, merged));
}
if (!merged.equals(local.get())) {
localUpdates.add(new RecordUpdate<>(local.get(), merged));
localUpdates.add(new StorageRecordUpdate<>(local.get(), merged));
}
localInserts.remove(remote);
@ -392,7 +418,7 @@ public final class StorageSyncHelper {
return new RecordMergeResult<>(localInserts, localUpdates, remoteInserts, remoteUpdates, remoteDeletes);
}
public static boolean profileKeyChanged(RecordUpdate<SignalContactRecord> update) {
public static boolean profileKeyChanged(StorageRecordUpdate<SignalContactRecord> update) {
return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey());
}
@ -439,15 +465,15 @@ public final class StorageSyncHelper {
return SignalStorageRecord.forAccount(account);
}
public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional<StorageSyncHelper.RecordUpdate<SignalAccountRecord>> update) {
public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional<StorageRecordUpdate<SignalAccountRecord>> update) {
if (!update.isPresent()) {
return;
}
applyAccountStorageSyncUpdates(context, StorageId.forAccount(Recipient.self().getStorageServiceId()), update.get().getNew(), true);
applyAccountStorageSyncUpdates(context, Recipient.self(), update.get().getNew(), true);
}
public static void applyAccountStorageSyncUpdates(@NonNull Context context, @NonNull StorageId storageId, @NonNull SignalAccountRecord update, boolean fetchProfile) {
DatabaseFactory.getRecipientDatabase(context).applyStorageSyncUpdates(storageId, update);
public static void applyAccountStorageSyncUpdates(@NonNull Context context, @NonNull Recipient self, @NonNull SignalAccountRecord update, boolean fetchProfile) {
DatabaseFactory.getRecipientDatabase(context).applyStorageSyncUpdates(StorageId.forAccount(self.getStorageServiceId()), update);
TextSecurePreferences.setReadReceiptsEnabled(context, update.isReadReceiptsEnabled());
TextSecurePreferences.setTypingIndicatorsEnabled(context, update.isTypingIndicatorsEnabled());
@ -459,7 +485,7 @@ public final class StorageSyncHelper {
SignalStore.paymentsValues().setEnabledAndEntropy(update.getPayments().isEnabled(), Entropy.fromBytes(update.getPayments().getEntropy().orNull()));
if (fetchProfile && update.getAvatarUrlPath().isPresent()) {
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), update.getAvatarUrlPath().get()));
ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(self, update.getAvatarUrlPath().get()));
}
}
@ -468,7 +494,7 @@ public final class StorageSyncHelper {
Log.d(TAG, "Registration still ongoing. Ignore sync request.");
return;
}
ApplicationDependencies.getJobManager().add(new StorageSyncJob());
ApplicationDependencies.getJobManager().add(StorageSyncJob.create());
}
public static void scheduleRoutineSync() {
@ -515,35 +541,40 @@ public final class StorageSyncHelper {
public boolean isEmpty() {
return remoteOnlyKeys.isEmpty() && localOnlyKeys.isEmpty();
}
@Override
public @NonNull String toString() {
return "remoteOnly: " + remoteOnlyKeys.size() + ", localOnly: " + localOnlyKeys.size() + ", hasTypeMismatches: " + hasTypeMismatches;
}
}
public static final class MergeResult {
private final Set<SignalContactRecord> localContactInserts;
private final Set<RecordUpdate<SignalContactRecord>> localContactUpdates;
private final Set<SignalGroupV1Record> localGroupV1Inserts;
private final Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
private final Set<SignalGroupV2Record> localGroupV2Inserts;
private final Set<RecordUpdate<SignalGroupV2Record>> localGroupV2Updates;
private final Set<SignalStorageRecord> localUnknownInserts;
private final Set<SignalStorageRecord> localUnknownDeletes;
private final Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate;
private final Set<SignalStorageRecord> remoteInserts;
private final Set<RecordUpdate<SignalStorageRecord>> remoteUpdates;
private final Set<SignalRecord> remoteDeletes;
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<RecordUpdate<SignalContactRecord>> localContactUpdates,
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
@NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
@NonNull Set<SignalGroupV2Record> localGroupV2Inserts,
@NonNull Set<RecordUpdate<SignalGroupV2Record>> localGroupV2Updates,
@NonNull Set<SignalStorageRecord> localUnknownInserts,
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
@NonNull Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate,
@NonNull Set<SignalStorageRecord> remoteInserts,
@NonNull Set<RecordUpdate<SignalStorageRecord>> remoteUpdates,
@NonNull Set<SignalRecord> remoteDeletes)
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;
@ -563,7 +594,7 @@ public final class StorageSyncHelper {
return localContactInserts;
}
public @NonNull Set<RecordUpdate<SignalContactRecord>> getLocalContactUpdates() {
public @NonNull Set<StorageRecordUpdate<SignalContactRecord>> getLocalContactUpdates() {
return localContactUpdates;
}
@ -571,7 +602,7 @@ public final class StorageSyncHelper {
return localGroupV1Inserts;
}
public @NonNull Set<RecordUpdate<SignalGroupV1Record>> getLocalGroupV1Updates() {
public @NonNull Set<StorageRecordUpdate<SignalGroupV1Record>> getLocalGroupV1Updates() {
return localGroupV1Updates;
}
@ -579,7 +610,7 @@ public final class StorageSyncHelper {
return localGroupV2Inserts;
}
public @NonNull Set<RecordUpdate<SignalGroupV2Record>> getLocalGroupV2Updates() {
public @NonNull Set<StorageRecordUpdate<SignalGroupV2Record>> getLocalGroupV2Updates() {
return localGroupV2Updates;
}
@ -591,7 +622,7 @@ public final class StorageSyncHelper {
return localUnknownDeletes;
}
public @NonNull Optional<RecordUpdate<SignalAccountRecord>> getLocalAccountUpdate() {
public @NonNull Optional<StorageRecordUpdate<SignalAccountRecord>> getLocalAccountUpdate() {
return localAccountUpdate;
}
@ -599,7 +630,7 @@ public final class StorageSyncHelper {
return remoteInserts;
}
public @NonNull Set<RecordUpdate<SignalStorageRecord>> getRemoteUpdates() {
public @NonNull Set<StorageRecordUpdate<SignalStorageRecord>> getRemoteUpdates() {
return remoteUpdates;
}
@ -615,10 +646,10 @@ public final class StorageSyncHelper {
records.addAll(localGroupV2Inserts);
records.addAll(remoteInserts);
records.addAll(localUnknownInserts);
records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getNew).toList());
records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getNew).toList());
records.addAll(Stream.of(localGroupV2Updates).map(RecordUpdate::getNew).toList());
records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getNew).toList());
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;
@ -628,10 +659,10 @@ public final class StorageSyncHelper {
Set<SignalRecord> records = new HashSet<>();
records.addAll(localUnknownDeletes);
records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getOld).toList());
records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getOld).toList());
records.addAll(Stream.of(localGroupV2Updates).map(RecordUpdate::getOld).toList());
records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getOld).toList());
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());
@ -705,50 +736,18 @@ public final class StorageSyncHelper {
}
}
public static class RecordUpdate<E extends SignalRecord> {
private final E oldRecord;
private final E newRecord;
RecordUpdate(@NonNull E oldRecord, @NonNull E newRecord) {
this.oldRecord = oldRecord;
this.newRecord = newRecord;
}
public @NonNull E getOld() {
return oldRecord;
}
public @NonNull E getNew() {
return newRecord;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RecordUpdate that = (RecordUpdate) o;
return oldRecord.equals(that.oldRecord) &&
newRecord.equals(that.newRecord);
}
@Override
public int hashCode() {
return Objects.hash(oldRecord, newRecord);
}
}
private static class RecordMergeResult<Record extends SignalRecord> {
final Set<Record> localInserts;
final Set<RecordUpdate<Record>> localUpdates;
final Set<Record> remoteInserts;
final Set<RecordUpdate<Record>> remoteUpdates;
final Set<Record> remoteDeletes;
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<RecordUpdate<Record>> localUpdates,
@NonNull Set<Record> remoteInserts,
@NonNull Set<RecordUpdate<Record>> remoteUpdates,
@NonNull 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;
@ -761,11 +760,7 @@ public final class StorageSyncHelper {
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 KeyGenerator keyGenerator);
}
interface KeyGenerator {
@NonNull byte[] generate();
@NonNull E merge(@NonNull E remote, @NonNull E local, @NonNull StorageKeyGenerator keyGenerator);
}
private static final class MultipleExistingAccountsException extends IllegalArgumentException {}

Wyświetl plik

@ -55,15 +55,15 @@ public final class StorageSyncValidations {
throw new DuplicateRawIdError();
}
if (inserts.size() > insertSet.size()) {
throw new DuplicateInsertInWriteError();
}
int accountCount = 0;
for (StorageId id : manifest.getStorageIds()) {
accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE ? 1 : 0;
}
if (inserts.size() > insertSet.size()) {
throw new DuplicateInsertInWriteError();
}
if (accountCount > 1) {
throw new MultipleAccountError();
}

Wyświetl plik

@ -74,6 +74,7 @@ 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";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@ -104,7 +105,8 @@ public final class FeatureFlags {
ANIMATED_STICKER_MIN_MEMORY,
ANIMATED_STICKER_MIN_TOTAL_MEMORY,
MESSAGE_PROCESSOR_ALARM_INTERVAL,
MESSAGE_PROCESSOR_DELAY
MESSAGE_PROCESSOR_DELAY,
STORAGE_SYNC_V2
);
@VisibleForTesting
@ -147,7 +149,8 @@ public final class FeatureFlags {
ANIMATED_STICKER_MIN_TOTAL_MEMORY,
MESSAGE_PROCESSOR_ALARM_INTERVAL,
MESSAGE_PROCESSOR_DELAY,
GV1_FORCED_MIGRATE
GV1_FORCED_MIGRATE,
STORAGE_SYNC_V2
);
/**
@ -334,6 +337,11 @@ 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, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

Wyświetl plik

@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.storage;
import org.junit.Test;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyGenerator;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
@ -70,7 +69,7 @@ public class ContactConflictMergerTest {
.setForcedUnread(true)
.build();
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class));
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());
@ -103,7 +102,7 @@ public class ContactConflictMergerTest {
.setUsername("username B")
.setProfileSharingEnabled(false)
.build();
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class));
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());
@ -133,7 +132,7 @@ public class ContactConflictMergerTest {
.setFamilyName("BLast")
.setProfileSharingEnabled(false)
.build();
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class));
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(StorageKeyGenerator.class));
assertEquals(remote, merged);
}
@ -145,7 +144,7 @@ public class ContactConflictMergerTest {
.setGivenName("AFirst")
.setFamilyName("ALast")
.build();
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class));
SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(StorageKeyGenerator.class));
assertEquals(local, merged);
}

Wyświetl plik

@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.storage;
import org.junit.Test;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyGenerator;
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record;
import java.util.Arrays;
@ -19,8 +18,8 @@ import static org.thoughtcrime.securesms.testutil.ZkGroupLibraryUtil.assumeZkGro
public final class GroupV1ConflictMergerTest {
private static final byte[] GENERATED_KEY = byteArray(8675309);
private static final KeyGenerator KEY_GENERATOR = mock(KeyGenerator.class);
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);
@ -64,7 +63,7 @@ public final class GroupV1ConflictMergerTest {
.setArchived(false)
.build();
SignalGroupV1Record merged = new GroupV1ConflictMerger(Collections.singletonList(local), id -> false).merge(remote, local, mock(KeyGenerator.class));
SignalGroupV1Record merged = new GroupV1ConflictMerger(Collections.singletonList(local), id -> false).merge(remote, local, mock(StorageKeyGenerator.class));
assertEquals(remote, merged);
}

Wyświetl plik

@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.storage;
import org.junit.Test;
import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyGenerator;
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record;
import java.util.Arrays;
@ -17,8 +16,8 @@ import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray;
public final class GroupV2ConflictMergerTest {
private static final byte[] GENERATED_KEY = byteArray(8675309);
private static final KeyGenerator KEY_GENERATOR = mock(KeyGenerator.class);
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);
@ -62,7 +61,7 @@ public final class GroupV2ConflictMergerTest {
.setArchived(false)
.build();
SignalGroupV2Record merged = new GroupV2ConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class));
SignalGroupV2Record merged = new GroupV2ConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(StorageKeyGenerator.class));
assertEquals(remote, merged);
}

Wyświetl plik

@ -569,12 +569,12 @@ public final class StorageSyncHelperTest {
return new SignalGroupV2Record.Builder(byteArray(key), byteArray(groupId, 42)).setBlocked(blocked).setProfileSharingEnabled(profileSharing).build();
}
private static <E extends SignalRecord> StorageSyncHelper.RecordUpdate<E> update(E oldRecord, E newRecord) {
return new StorageSyncHelper.RecordUpdate<>(oldRecord, newRecord);
private static <E extends SignalRecord> StorageRecordUpdate<E> update(E oldRecord, E newRecord) {
return new StorageRecordUpdate<>(oldRecord, newRecord);
}
private static <E extends SignalRecord> StorageSyncHelper.RecordUpdate<SignalStorageRecord> recordUpdate(E oldContact, E newContact) {
return new StorageSyncHelper.RecordUpdate<>(record(oldContact), record(newContact));
private static <E extends SignalRecord> StorageRecordUpdate<SignalStorageRecord> recordUpdate(E oldContact, E newContact) {
return new StorageRecordUpdate<>(record(oldContact), record(newContact));
}
private static SignalStorageRecord unknown(int key) {
@ -605,7 +605,7 @@ public final class StorageSyncHelperTest {
return StorageId.forType(byteArray(val), UNKNOWN_TYPE);
}
private static class TestGenerator implements StorageSyncHelper.KeyGenerator {
private static class TestGenerator implements StorageKeyGenerator {
private final int[] keys;
private int index = 0;

Wyświetl plik

@ -12,6 +12,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
@ -52,6 +53,87 @@ public final class SignalAccountRecord implements SignalRecord {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forAccount(this);
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalAccountRecord) {
SignalAccountRecord that = (SignalAccountRecord) other;
List<String> diff = new LinkedList<>();
if (!Objects.equals(this.givenName, that.givenName)) {
diff.add("GivenName");
}
if (!Objects.equals(this.familyName, that.familyName)) {
diff.add("FamilyName");
}
if (!OptionalUtil.byteArrayEquals(this.profileKey, that.profileKey)) {
diff.add("ProfileKey");
}
if (!Objects.equals(this.avatarUrlPath, that.avatarUrlPath)) {
diff.add("AvatarUrlPath");
}
if (!Objects.equals(this.isNoteToSelfArchived(), that.isNoteToSelfArchived())) {
diff.add("NoteToSelfArchived");
}
if (!Objects.equals(this.isNoteToSelfForcedUnread(), that.isNoteToSelfForcedUnread())) {
diff.add("NoteToSelfForcedUnread");
}
if (!Objects.equals(this.isReadReceiptsEnabled(), that.isReadReceiptsEnabled())) {
diff.add("ReadReceipts");
}
if (!Objects.equals(this.isTypingIndicatorsEnabled(), that.isTypingIndicatorsEnabled())) {
diff.add("TypingIndicators");
}
if (!Objects.equals(this.isSealedSenderIndicatorsEnabled(), that.isSealedSenderIndicatorsEnabled())) {
diff.add("SealedSenderIndicators");
}
if (!Objects.equals(this.isLinkPreviewsEnabled(), that.isLinkPreviewsEnabled())) {
diff.add("LinkPreviews");
}
if (!Objects.equals(this.getPhoneNumberSharingMode(), that.getPhoneNumberSharingMode())) {
diff.add("PhoneNumberSharingMode");
}
if (!Objects.equals(this.isPhoneNumberUnlisted(), that.isPhoneNumberUnlisted())) {
diff.add("PhoneNumberUnlisted");
}
if (!Objects.equals(this.pinnedConversations, that.pinnedConversations)) {
diff.add("PinnedConversations");
}
if (!Objects.equals(this.preferContactAvatars, that.preferContactAvatars)) {
diff.add("PreferContactAvatars");
}
if (!Objects.equals(this.payments, that.payments)) {
diff.add("PreferContactAvatars");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}
@ -251,6 +333,20 @@ public final class SignalAccountRecord implements SignalRecord {
public Optional<byte[]> getEntropy() {
return entropy;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Payments payments = (Payments) o;
return enabled == payments.enabled &&
OptionalUtil.byteArrayEquals(entropy, payments.entropy);
}
@Override
public int hashCode() {
return Objects.hash(enabled, entropy);
}
}
public static final class Builder {

Wyświetl plik

@ -10,6 +10,8 @@ import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
public final class SignalContactRecord implements SignalRecord {
@ -43,6 +45,75 @@ public final class SignalContactRecord implements SignalRecord {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forContact(this);
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalContactRecord) {
SignalContactRecord that = (SignalContactRecord) other;
List<String> diff = new LinkedList<>();
if (!Objects.equals(this.getAddress().getNumber(), that.getAddress().getNumber())) {
diff.add("E164");
}
if (!Objects.equals(this.getAddress().getUuid(), that.getAddress().getUuid())) {
diff.add("UUID");
}
if (!Objects.equals(this.givenName, that.givenName)) {
diff.add("GivenName");
}
if (!Objects.equals(this.familyName, that.familyName)) {
diff.add("FamilyName");
}
if (!OptionalUtil.byteArrayEquals(this.profileKey, that.profileKey)) {
diff.add("ProfileKey");
}
if (!Objects.equals(this.username, that.username)) {
diff.add("Username");
}
if (!OptionalUtil.byteArrayEquals(this.identityKey, that.identityKey)) {
diff.add("IdentityKey");
}
if (!Objects.equals(this.getIdentityState(), that.getIdentityState())) {
diff.add("IdentityState");
}
if (!Objects.equals(this.isBlocked(), that.isBlocked())) {
diff.add("Blocked");
}
if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) {
diff.add("ProfileSharing");
}
if (!Objects.equals(this.isArchived(), that.isArchived())) {
diff.add("Archived");
}
if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) {
diff.add("ForcedUnread");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}

Wyświetl plik

@ -5,6 +5,9 @@ import com.google.protobuf.ByteString;
import org.whispersystems.signalservice.api.util.ProtoUtil;
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
public final class SignalGroupV1Record implements SignalRecord {
@ -26,6 +29,47 @@ public final class SignalGroupV1Record implements SignalRecord {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forGroupV1(this);
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalGroupV1Record) {
SignalGroupV1Record that = (SignalGroupV1Record) other;
List<String> diff = new LinkedList<>();
if (!Arrays.equals(this.groupId, that.groupId)) {
diff.add("MasterKey");
}
if (!Objects.equals(this.isBlocked(), that.isBlocked())) {
diff.add("Blocked");
}
if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) {
diff.add("ProfileSharing");
}
if (!Objects.equals(this.isArchived(), that.isArchived())) {
diff.add("Archived");
}
if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) {
diff.add("ForcedUnread");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}

Wyświetl plik

@ -7,6 +7,9 @@ import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.signalservice.api.util.ProtoUtil;
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
public final class SignalGroupV2Record implements SignalRecord {
@ -28,6 +31,47 @@ public final class SignalGroupV2Record implements SignalRecord {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return SignalStorageRecord.forGroupV2(this);
}
@Override
public String describeDiff(SignalRecord other) {
if (other instanceof SignalGroupV2Record) {
SignalGroupV2Record that = (SignalGroupV2Record) other;
List<String> diff = new LinkedList<>();
if (!Arrays.equals(this.getMasterKeyBytes(), that.getMasterKeyBytes())) {
diff.add("MasterKey");
}
if (!Objects.equals(this.isBlocked(), that.isBlocked())) {
diff.add("Blocked");
}
if (!Objects.equals(this.isProfileSharingEnabled(), that.isProfileSharingEnabled())) {
diff.add("ProfileSharing");
}
if (!Objects.equals(this.isArchived(), that.isArchived())) {
diff.add("Archived");
}
if (!Objects.equals(this.isForcedUnread(), that.isForcedUnread())) {
diff.add("ForcedUnread");
}
if (!Objects.equals(this.hasUnknownFields(), that.hasUnknownFields())) {
diff.add("UnknownFields");
}
return diff.toString();
} else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
}
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}

Wyświetl plik

@ -2,4 +2,6 @@ package org.whispersystems.signalservice.api.storage;
public interface SignalRecord {
StorageId getId();
SignalStorageRecord asStorageRecord();
String describeDiff(SignalRecord other);
}

Wyświetl plik

@ -67,6 +67,16 @@ public class SignalStorageRecord implements SignalRecord {
return id;
}
@Override
public SignalStorageRecord asStorageRecord() {
return this;
}
@Override
public String describeDiff(SignalRecord other) {
return "Diffs not supported.";
}
public int getType() {
return id.getType();
}

Wyświetl plik

@ -29,6 +29,10 @@ public class StorageId {
return new StorageId(type, raw);
}
public boolean isUnknown() {
return !isKnownType(type);
}
private StorageId(int type, byte[] raw) {
this.type = type;
this.raw = raw;