Rewrite storage service change processing.
This commit is contained in:
parent
552b19cbb0
commit
0e200b1fb6
42 changed files with 1913 additions and 208 deletions
|
@ -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" />
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 + "/";
|
||||
|
|
|
@ -117,8 +117,6 @@ public final class DatabaseObserver {
|
|||
listener.onChanged();
|
||||
}
|
||||
});
|
||||
|
||||
application.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
|
||||
}
|
||||
|
||||
public void notifyPaymentListeners(@NonNull UUID paymentId) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -2,4 +2,6 @@ package org.whispersystems.signalservice.api.storage;
|
|||
|
||||
public interface SignalRecord {
|
||||
StorageId getId();
|
||||
SignalStorageRecord asStorageRecord();
|
||||
String describeDiff(SignalRecord other);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue