From cdc7f1565e00b69bc22b2f0e014c7b3d13ebb37e Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 28 Apr 2021 10:57:25 -0400 Subject: [PATCH] Further simplify storage service syncing. --- .../securesms/PassphraseRequiredActivity.java | 2 +- .../securesms/database/IdentityDatabase.java | 6 +- .../securesms/database/RecipientDatabase.java | 262 ++++++------------ .../database/helpers/SQLCipherOpenHelper.java | 44 ++- .../jobs/MultiDeviceKeysUpdateJob.java | 2 +- .../jobs/StorageAccountRestoreJob.java | 6 +- .../securesms/jobs/StorageForcePushJob.java | 4 +- .../securesms/jobs/StorageSyncJob.java | 196 ++++--------- .../securesms/keyvalue/SignalStore.java | 6 +- .../keyvalue/StorageServiceValues.java | 17 ++ .../securesms/logsubmit/LogSectionPin.java | 2 +- .../securesms/pin/PinRestoreViewModel.java | 2 +- .../thoughtcrime/securesms/pin/PinState.java | 6 +- .../RegistrationCompleteFragment.java | 2 +- .../securesms/storage/StorageSyncHelper.java | 156 +---------- .../storage/StorageSyncValidations.java | 10 +- .../securesms/util/TextSecurePreferences.java | 4 - .../api/storage/SignalStorageManifest.java | 41 +++ 18 files changed, 261 insertions(+), 507 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java index 88aa5de48b..eaa3f8109a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/PassphraseRequiredActivity.java @@ -166,7 +166,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements return STATE_UI_BLOCKING_UPGRADE; } else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) { return STATE_WELCOME_PUSH_SCREEN; - } else if (SignalStore.storageServiceValues().needsAccountRestore()) { + } else if (SignalStore.storageService().needsAccountRestore()) { return STATE_ENTER_SIGNAL_PIN; } else if (userMustSetProfileName()) { return STATE_CREATE_PROFILE_NAME; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java index b9c9cf7c4b..728ad33341 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/IdentityDatabase.java @@ -143,7 +143,7 @@ public class IdentityDatabase extends Database { boolean firstUse, long timestamp, boolean nonBlockingApproval) { saveIdentityInternal(recipientId, identityKey, verifiedStatus, firstUse, timestamp, nonBlockingApproval); - DatabaseFactory.getRecipientDatabase(context).markDirty(recipientId, RecipientDatabase.DirtyState.UPDATE); + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId); } public void setApproval(@NonNull RecipientId recipientId, boolean nonBlockingApproval) { @@ -154,7 +154,7 @@ public class IdentityDatabase extends Database { database.update(TABLE_NAME, contentValues, RECIPIENT_ID + " = ?", new String[] {recipientId.serialize()}); - DatabaseFactory.getRecipientDatabase(context).markDirty(recipientId, RecipientDatabase.DirtyState.UPDATE); + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId); } public void setVerified(@NonNull RecipientId recipientId, IdentityKey identityKey, VerifiedStatus verifiedStatus) { @@ -169,7 +169,7 @@ public class IdentityDatabase extends Database { if (updated > 0) { Optional record = getIdentity(recipientId); if (record.isPresent()) EventBus.getDefault().post(record.get()); - DatabaseFactory.getRecipientDatabase(context).markDirty(recipientId, RecipientDatabase.DirtyState.UPDATE); + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipientId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index ae93257bb5..94a877b762 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -8,7 +8,6 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.arch.core.util.Function; import com.annimon.stream.Stream; import com.google.protobuf.ByteString; @@ -90,6 +89,7 @@ import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Function; public class RecipientDatabase extends Database { @@ -132,7 +132,6 @@ public class RecipientDatabase extends Database { static final String FORCE_SMS_SELECTION = "force_sms_selection"; private static final String CAPABILITIES = "capabilities"; private static final String STORAGE_SERVICE_ID = "storage_service_key"; - private static final String DIRTY = "dirty"; private static final String PROFILE_GIVEN_NAME = "signal_profile_name"; private static final String PROFILE_FAMILY_NAME = "profile_family_name"; private static final String PROFILE_JOINED_NAME = "profile_joined_name"; @@ -169,7 +168,7 @@ public class RecipientDatabase extends Database { UNIDENTIFIED_ACCESS_MODE, FORCE_SMS_SELECTION, CAPABILITIES, - STORAGE_SERVICE_ID, DIRTY, + STORAGE_SERVICE_ID, MENTION_SETTING, WALLPAPER, WALLPAPER_URI, MENTION_SETTING, ABOUT, ABOUT_EMOJI, @@ -188,7 +187,6 @@ public class RecipientDatabase extends Database { private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_JOINED_NAME) + ", " + nullIfEmpty(SYSTEM_GIVEN_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME}; public static final String[] CREATE_INDEXS = new String[] { - "CREATE INDEX IF NOT EXISTS recipient_dirty_index ON " + TABLE_NAME + " (" + DIRTY + ");", "CREATE INDEX IF NOT EXISTS recipient_group_type_index ON " + TABLE_NAME + " (" + GROUP_TYPE + ");", }; @@ -272,24 +270,6 @@ public class RecipientDatabase extends Database { } } - public enum DirtyState { - CLEAN(0), UPDATE(1), INSERT(2), DELETE(3); - - private final int id; - - DirtyState(int id) { - this.id = id; - } - - int getId() { - return id; - } - - public static DirtyState fromId(int id) { - return values()[id]; - } - } - public enum GroupType { NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3); @@ -365,7 +345,6 @@ public class RecipientDatabase extends Database { UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " + FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " + STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " + - DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " + MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.ALWAYS_NOTIFY.getId() + ", " + STORAGE_PROTO + " TEXT DEFAULT NULL, " + CAPABILITIES + " INTEGER DEFAULT 0, " + @@ -404,24 +383,29 @@ public class RecipientDatabase extends Database { } } - public @NonNull Optional getByE164(@NonNull String e164) { + public @NonNull + Optional getByE164(@NonNull String e164) { return getByColumn(PHONE, e164); } - public @NonNull Optional getByEmail(@NonNull String email) { + public @NonNull + Optional getByEmail(@NonNull String email) { return getByColumn(EMAIL, email); } - public @NonNull Optional getByGroupId(@NonNull GroupId groupId) { + public @NonNull + Optional getByGroupId(@NonNull GroupId groupId) { return getByColumn(GROUP_ID, groupId.toString()); } - public @NonNull Optional getByUuid(@NonNull UUID uuid) { + public @NonNull + Optional getByUuid(@NonNull UUID uuid) { return getByColumn(UUID, uuid.toString()); } - public @NonNull Optional getByUsername(@NonNull String username) { + public @NonNull + Optional getByUsername(@NonNull String username) { return getByColumn(USERNAME, username); } @@ -568,7 +552,6 @@ public class RecipientDatabase extends Database { if (uuid != null) { values.put(UUID, uuid.toString().toLowerCase()); values.put(REGISTERED, RegisteredState.REGISTERED.getId()); - values.put(DIRTY, DirtyState.INSERT.getId()); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); } @@ -626,7 +609,6 @@ public class RecipientDatabase extends Database { } else { groupUpdates.put(GROUP_TYPE, GroupType.SIGNAL_V1.getId()); } - groupUpdates.put(DIRTY, DirtyState.INSERT.getId()); groupUpdates.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); } @@ -718,18 +700,6 @@ public class RecipientDatabase extends Database { } } - public @NonNull DirtyState getDirtyState(@NonNull RecipientId recipientId) { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - - try (Cursor cursor = db.query(TABLE_NAME, new String[] { DIRTY }, ID_WHERE, new String[] { recipientId.serialize() }, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - return DirtyState.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(DIRTY))); - } - } - - return DirtyState.CLEAN; - } - public @Nullable RecipientSettings getRecipientSettingsForSync(@NonNull RecipientId id) { String query = TABLE_NAME + "." + ID + " = ?"; String[] args = new String[]{id.serialize()}; @@ -747,27 +717,6 @@ public class RecipientDatabase extends Database { return recipientSettingsForSync.get(0); } - public @NonNull List getPendingRecipientSyncUpdates() { - String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; - String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() }; - - return getRecipientSettingsForSync(query, args); - } - - public @NonNull List getPendingRecipientSyncInsertions() { - String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; - String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() }; - - return getRecipientSettingsForSync(query, args); - } - - public @NonNull List getPendingRecipientSyncDeletions() { - String query = TABLE_NAME + "." + DIRTY + " = ? AND " + TABLE_NAME + "." + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; - String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize() }; - - return getRecipientSettingsForSync(query, args); - } - public @Nullable RecipientSettings getByStorageId(@NonNull byte[] storageId) { List result = getRecipientSettingsForSync(TABLE_NAME + "." + STORAGE_SERVICE_ID + " = ?", new String[] { Base64.encodeBytes(storageId) }); @@ -784,7 +733,8 @@ public class RecipientDatabase extends Database { db.beginTransaction(); try { for (RecipientId recipientId : recipientIds) { - markDirty(recipientId, DirtyState.UPDATE); + rotateStorageId(recipientId); + Recipient.live(recipientId).refresh(); } db.setTransactionSuccessful(); } finally { @@ -793,7 +743,8 @@ public class RecipientDatabase extends Database { } public void markNeedsSync(@NonNull RecipientId recipientId) { - markDirty(recipientId, DirtyState.UPDATE); + rotateStorageId(recipientId); + Recipient.live(recipientId).refresh(); } public void applyStorageIdUpdates(@NonNull Map storageIds) { @@ -806,7 +757,6 @@ public class RecipientDatabase extends Database { for (Map.Entry entry : storageIds.entrySet()) { ContentValues values = new ContentValues(); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(entry.getValue().getRaw())); - values.put(DIRTY, DirtyState.CLEAN.getId()); db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() }); } @@ -996,7 +946,6 @@ public class RecipientDatabase extends Database { values.put(PROFILE_JOINED_NAME, profileName.toString()); values.put(PROFILE_KEY, profileKey); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getId().getRaw())); - values.put(DIRTY, DirtyState.CLEAN.getId()); if (update.hasUnknownFields()) { values.put(STORAGE_PROTO, Base64.encodeBytes(update.serializeUnknownFields())); @@ -1075,7 +1024,6 @@ public class RecipientDatabase extends Database { values.put(BLOCKED, contact.isBlocked() ? "1" : "0"); values.put(MUTE_UNTIL, contact.getMuteUntil()); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw())); - values.put(DIRTY, DirtyState.CLEAN.getId()); if (contact.isProfileSharingEnabled() && isInsert && !profileName.isEmpty()) { values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize()); @@ -1098,7 +1046,6 @@ public class RecipientDatabase extends Database { values.put(BLOCKED, groupV1.isBlocked() ? "1" : "0"); values.put(MUTE_UNTIL, groupV1.getMuteUntil()); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.getId().getRaw())); - values.put(DIRTY, DirtyState.CLEAN.getId()); if (groupV1.hasUnknownFields()) { values.put(STORAGE_PROTO, Base64.encodeBytes(groupV1.serializeUnknownFields())); @@ -1117,7 +1064,6 @@ public class RecipientDatabase extends Database { values.put(BLOCKED, groupV2.isBlocked() ? "1" : "0"); values.put(MUTE_UNTIL, groupV2.getMuteUntil()); values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.getId().getRaw())); - values.put(DIRTY, DirtyState.CLEAN.getId()); if (groupV2.hasUnknownFields()) { values.put(STORAGE_PROTO, Base64.encodeBytes(groupV2.serializeUnknownFields())); @@ -1166,8 +1112,8 @@ public class RecipientDatabase extends Database { */ public @NonNull Map getContactStorageSyncIdsMap() { SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ? AND " + ID + " != ? AND " + GROUP_TYPE + " != ?"; - String[] args = { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize(), String.valueOf(GroupType.SIGNAL_V2.getId()) }; + String query = STORAGE_SERVICE_ID + " NOT NULL AND " + ID + " != ? AND " + GROUP_TYPE + " != ?"; + String[] args = SqlUtil.buildArgs(Recipient.self().getId(), String.valueOf(GroupType.SIGNAL_V2.getId())); Map out = new HashMap<>(); try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_ID, GROUP_TYPE }, query, args, null, null, null)) { @@ -1432,7 +1378,7 @@ public class RecipientDatabase extends Database { ContentValues values = new ContentValues(); values.put(BLOCKED, blocked ? 1 : 0); if (update(id, values)) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); } } @@ -1473,8 +1419,8 @@ public class RecipientDatabase extends Database { ContentValues values = new ContentValues(); values.put(MUTE_UNTIL, until); if (update(id, values)) { + rotateStorageId(id); Recipient.live(id).refresh(); - markDirty(id, DirtyState.UPDATE); } StorageSyncHelper.scheduleSyncForDataChange(); } @@ -1514,7 +1460,7 @@ public class RecipientDatabase extends Database { ContentValues values = new ContentValues(1); values.put(UNIDENTIFIED_ACCESS_MODE, unidentifiedAccessMode.getMode()); if (update(id, values)) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); } } @@ -1612,7 +1558,7 @@ public class RecipientDatabase extends Database { SqlUtil.Query updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare); if (update(updateQuery, valuesToSet)) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); StorageSyncHelper.scheduleSyncForDataChange(); return true; @@ -1637,7 +1583,7 @@ public class RecipientDatabase extends Database { valuesToSet.put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.getMode()); if (database.update(TABLE_NAME, valuesToSet, selection, args) > 0) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); return true; } else { @@ -1678,7 +1624,7 @@ public class RecipientDatabase extends Database { ContentValues values = new ContentValues(1); values.putNull(PROFILE_KEY_CREDENTIAL); if (update(id, values)) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); } } @@ -1760,7 +1706,7 @@ public class RecipientDatabase extends Database { contentValues.put(PROFILE_FAMILY_NAME, profileName.getFamilyName()); contentValues.put(PROFILE_JOINED_NAME, profileName.toString()); if (update(id, contentValues)) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); StorageSyncHelper.scheduleSyncForDataChange(); } @@ -1773,7 +1719,7 @@ public class RecipientDatabase extends Database { Recipient.live(id).refresh(); if (id.equals(Recipient.self().getId())) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); StorageSyncHelper.scheduleSyncForDataChange(); } } @@ -1805,7 +1751,7 @@ public class RecipientDatabase extends Database { } if (profiledUpdated || colorUpdated) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); StorageSyncHelper.scheduleSyncForDataChange(); } @@ -1986,7 +1932,7 @@ public class RecipientDatabase extends Database { contentValues.put(PHONE, e164); if (update(id, contentValues)) { - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); StorageSyncHelper.scheduleSyncForDataChange(); } @@ -2069,7 +2015,7 @@ public class RecipientDatabase extends Database { contentValues.put(UUID, uuid.toString().toLowerCase()); if (update(id, contentValues)) { - markDirty(id, DirtyState.INSERT); + setStorageIdIfNotSet(id); Recipient.live(id).refresh(); } } @@ -2082,8 +2028,9 @@ public class RecipientDatabase extends Database { public void markRegistered(@NonNull RecipientId id) { ContentValues contentValues = new ContentValues(1); contentValues.put(REGISTERED, RegisteredState.REGISTERED.getId()); + if (update(id, contentValues)) { - markDirty(id, DirtyState.INSERT); + setStorageIdIfNotSet(id); Recipient.live(id).refresh(); } } @@ -2091,8 +2038,9 @@ public class RecipientDatabase extends Database { public void markUnregistered(@NonNull RecipientId id) { ContentValues contentValues = new ContentValues(2); contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId()); + contentValues.putNull(STORAGE_SERVICE_ID); + if (update(id, contentValues)) { - markDirty(id, DirtyState.DELETE); Recipient.live(id).refresh(); } } @@ -2112,7 +2060,7 @@ public class RecipientDatabase extends Database { try { if (update(entry.getKey(), values)) { - markDirty(entry.getKey(), DirtyState.INSERT); + setStorageIdIfNotSet(entry.getKey()); } } catch (SQLiteConstraintException e) { Log.w(TAG, "[bulkUpdateRegisteredStatus] Hit a conflict when trying to update " + entry.getKey() + ". Possibly merging."); @@ -2126,9 +2074,9 @@ public class RecipientDatabase extends Database { for (RecipientId id : unregistered) { ContentValues values = new ContentValues(2); values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId()); - if (update(id, values)) { - markDirty(id, DirtyState.DELETE); - } + values.putNull(STORAGE_SERVICE_ID); + + update(id, values); } db.setTransactionSuccessful(); @@ -2142,11 +2090,13 @@ public class RecipientDatabase extends Database { ContentValues contentValues = new ContentValues(1); contentValues.put(REGISTERED, registeredState.getId()); + if (registeredState == RegisteredState.NOT_REGISTERED) { + contentValues.putNull(STORAGE_SERVICE_ID); + } + if (update(id, contentValues)) { if (registeredState == RegisteredState.REGISTERED) { - markDirty(id, DirtyState.INSERT); - } else if (registeredState == RegisteredState.NOT_REGISTERED) { - markDirty(id, DirtyState.DELETE); + setStorageIdIfNotSet(id); } Recipient.live(id).refresh(); @@ -2162,7 +2112,7 @@ public class RecipientDatabase extends Database { registeredValues.put(REGISTERED, RegisteredState.REGISTERED.getId()); if (update(activeId, registeredValues)) { - markDirty(activeId, DirtyState.INSERT); + setStorageIdIfNotSet(activeId); Recipient.live(activeId).refresh(); } } @@ -2170,9 +2120,9 @@ public class RecipientDatabase extends Database { for (RecipientId inactiveId : inactiveIds) { ContentValues contentValues = new ContentValues(1); contentValues.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId()); + contentValues.putNull(STORAGE_SERVICE_ID); if (update(inactiveId, contentValues)) { - markDirty(inactiveId, DirtyState.DELETE); Recipient.live(inactiveId).refresh(); } } @@ -2619,46 +2569,6 @@ public class RecipientDatabase extends Database { } } - public void clearDirtyStateForStorageIds(@NonNull Collection ids) { - 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 (StorageId id : ids) { - String[] args = SqlUtil.buildArgs(Base64.encodeBytes(id.getRaw())); - db.update(TABLE_NAME, values, query, args); - } - } - - public void clearDirtyState(@NonNull List recipients) { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - db.beginTransaction(); - - try { - ContentValues values = new ContentValues(); - values.put(DIRTY, DirtyState.CLEAN.getId()); - - for (RecipientId id : recipients) { - Optional remapped = RemappedRecords.getInstance().getRecipient(context, id); - if (remapped.isPresent()) { - Log.w(TAG, "While clearing dirty state, noticed we have a remapped contact (" + id + " to " + remapped.get() + "). Safe to delete now."); - db.delete(TABLE_NAME, ID_WHERE, new String[]{id.serialize()}); - } else { - db.update(TABLE_NAME, values, ID_WHERE, new String[]{id.serialize()}); - } - } - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - public void markPreMessageRequestRecipientsAsProfileSharingEnabled(long messageRequestEnableTime) { String[] whereArgs = SqlUtil.buildArgs(messageRequestEnableTime, messageRequestEnableTime); @@ -2750,31 +2660,27 @@ public class RecipientDatabase extends Database { Recipient.live(recipientId).refresh(); } - void markDirty(@NonNull RecipientId recipientId, @NonNull DirtyState dirtyState) { - Log.d(TAG, "Attempting to mark " + recipientId + " with dirty state " + dirtyState); + /** + * Does not trigger any recipient refreshes -- it is assumed the caller handles this. + */ + void rotateStorageId(@NonNull RecipientId recipientId) { + ContentValues values = new ContentValues(1); + values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); - ContentValues contentValues = new ContentValues(1); - contentValues.put(DIRTY, dirtyState.getId()); + databaseHelper.getWritableDatabase().update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId)); + } - String query = ID + " = ? AND (" + UUID + " NOT NULL OR " + PHONE + " NOT NULL OR " + GROUP_ID + " NOT NULL) AND "; - String[] args = new String[] { recipientId.serialize(), String.valueOf(dirtyState.id) }; + /** + * Does not trigger any recipient refreshes -- it is assumed the caller handles this. + */ + void setStorageIdIfNotSet(@NonNull RecipientId recipientId) { + ContentValues values = new ContentValues(1); + values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); - switch (dirtyState) { - case INSERT: - query += "(" + DIRTY + " < ? OR " + DIRTY + " = ?)"; - args = SqlUtil.appendArg(args, String.valueOf(DirtyState.DELETE.getId())); + String query = ID + " = ? AND " + STORAGE_SERVICE_ID + " IS NULL"; + String[] args = SqlUtil.buildArgs(recipientId); - contentValues.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey())); - break; - case DELETE: - query += "(" + DIRTY + " < ? OR " + DIRTY + " = ?)"; - args = SqlUtil.appendArg(args, String.valueOf(DirtyState.INSERT.getId())); - break; - default: - query += DIRTY + " < ?"; - } - - databaseHelper.getWritableDatabase().update(TABLE_NAME, contentValues, query, args); + databaseHelper.getWritableDatabase().update(TABLE_NAME, values, query, args); } /** @@ -2790,7 +2696,7 @@ public class RecipientDatabase extends Database { if (update(query, values)) { RecipientId id = getByGroupId(v2Id).get(); - markDirty(id, DirtyState.UPDATE); + rotateStorageId(id); Recipient.live(id).refresh(); } } @@ -2816,7 +2722,8 @@ public class RecipientDatabase extends Database { return database.update(TABLE_NAME, contentValues, updateQuery.getWhere(), updateQuery.getWhereArgs()) > 0; } - private @NonNull Optional getByColumn(@NonNull String column, String value) { + private @NonNull + Optional getByColumn(@NonNull String column, String value) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); String query = column + " = ?"; String[] args = new String[] { value }; @@ -2873,17 +2780,8 @@ public class RecipientDatabase extends Database { RecipientSettings e164Settings = getRecipientSettings(byE164); // Recipient - if (e164Settings.getStorageId() == null) { - Log.w(TAG, "No storageId on the E164 recipient. Can delete right away."); - db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164)); - } else { - Log.w(TAG, "The E164 recipient has a storageId. Clearing data and marking for deletion."); - ContentValues values = new ContentValues(); - values.putNull(PHONE); - values.put(REGISTERED, RegisteredState.NOT_REGISTERED.getId()); - values.put(DIRTY, DirtyState.DELETE.getId()); - db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(byE164)); - } + Log.w(TAG, "Deleting recipient " + byE164); + db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164)); RemappedRecords.getInstance().addRecipient(context, byE164, byUuid); ContentValues uuidValues = new ContentValues(); @@ -3020,18 +2918,12 @@ public class RecipientDatabase extends Database { int systemPhoneType, @Nullable String systemContactUri) { - ContentValues dirtyQualifyingValues = new ContentValues(); - String joinedName = Util.firstNonNull(systemDisplayName, systemProfileName.toString()); - - dirtyQualifyingValues.put(SYSTEM_GIVEN_NAME, systemProfileName.getGivenName()); - dirtyQualifyingValues.put(SYSTEM_FAMILY_NAME, systemProfileName.getFamilyName()); - dirtyQualifyingValues.put(SYSTEM_JOINED_NAME, joinedName); - - if (update(id, dirtyQualifyingValues)) { - markDirty(id, DirtyState.UPDATE); - } + String joinedName = Util.firstNonNull(systemDisplayName, systemProfileName.toString()); ContentValues refreshQualifyingValues = new ContentValues(); + refreshQualifyingValues.put(SYSTEM_GIVEN_NAME, systemProfileName.getGivenName()); + refreshQualifyingValues.put(SYSTEM_FAMILY_NAME, systemProfileName.getFamilyName()); + refreshQualifyingValues.put(SYSTEM_JOINED_NAME, joinedName); refreshQualifyingValues.put(SYSTEM_PHOTO_URI, photoUri); refreshQualifyingValues.put(SYSTEM_PHONE_LABEL, systemPhoneLabel); refreshQualifyingValues.put(SYSTEM_PHONE_TYPE, systemPhoneType); @@ -3060,13 +2952,15 @@ public class RecipientDatabase extends Database { } private void markAllRelevantEntriesDirty() { - String query = SYSTEM_INFO_PENDING + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " < ?"; - String[] args = new String[] { "1", String.valueOf(DirtyState.UPDATE.getId()) }; + String query = SYSTEM_INFO_PENDING + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL"; + String[] args = SqlUtil.buildArgs("1"); - ContentValues values = new ContentValues(1); - values.put(DIRTY, DirtyState.UPDATE.getId()); - - database.update(TABLE_NAME, values, query, args); + try (Cursor cursor = database.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null)) { + while (cursor.moveToNext()) { + RecipientId id = RecipientId.from(CursorUtil.requireString(cursor, ID)); + rotateStorageId(id); + } + } } private void clearSystemDataForPendingInfo() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 7991943168..16b078b812 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -178,8 +178,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int BLUR_AVATARS = 94; private static final int CLEAN_STORAGE_IDS_WITHOUT_INFO = 95; private static final int CLEAN_REACTION_NOTIFICATIONS = 96; + private static final int STORAGE_SERVICE_REFACTOR = 97; - private static final int DATABASE_VERSION = 96; + private static final int DATABASE_VERSION = 97; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -1396,6 +1397,47 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab } } + if (oldVersion < STORAGE_SERVICE_REFACTOR) { + int deleteCount; + int insertCount; + int updateCount; + int dirtyCount; + + ContentValues deleteValues = new ContentValues(); + deleteValues.putNull("storage_service_key"); + deleteCount = db.update("recipient", deleteValues, "storage_service_key NOT NULL AND (dirty = 3 OR group_type = 1 OR (group_type = 0 AND registered = 2))", null); + + try (Cursor cursor = db.query("recipient", new String[]{"_id"}, "storage_service_key IS NULL AND (dirty = 2 OR registered = 1)", null, null, null, null)) { + insertCount = cursor.getCount(); + + while (cursor.moveToNext()) { + ContentValues insertValues = new ContentValues(); + insertValues.put("storage_service_key", Base64.encodeBytes(StorageSyncHelper.generateKey())); + + long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); + db.update("recipient", insertValues, "_id = ?", SqlUtil.buildArgs(id)); + } + } + + try (Cursor cursor = db.query("recipient", new String[]{"_id"}, "storage_service_key NOT NULL AND dirty = 1", null, null, null, null)) { + updateCount = cursor.getCount(); + + while (cursor.moveToNext()) { + ContentValues updateValues = new ContentValues(); + updateValues.put("storage_service_key", Base64.encodeBytes(StorageSyncHelper.generateKey())); + + long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); + db.update("recipient", updateValues, "_id = ?", SqlUtil.buildArgs(id)); + } + } + + ContentValues clearDirtyValues = new ContentValues(); + clearDirtyValues.put("dirty", 0); + dirtyCount = db.update("recipient", clearDirtyValues, "dirty != 0", null); + + Log.d(TAG, String.format(Locale.US, "For storage service refactor migration, there were %d inserts, %d updated, and %d deletes. Cleared the dirty status on %d rows.", insertCount, updateCount, deleteCount, dirtyCount)); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java index d1df13e1c7..c6ea2ac29d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/MultiDeviceKeysUpdateJob.java @@ -59,7 +59,7 @@ public class MultiDeviceKeysUpdateJob extends BaseJob { } SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender(); - StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey(); + StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey(); messageSender.sendMessage(SignalServiceSyncMessage.forKeys(new KeysMessage(Optional.fromNullable(storageServiceKey))), UnidentifiedAccessUtil.getAccessForSync(context)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java index b5946875ed..53cc93a8fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java @@ -65,7 +65,7 @@ public class StorageAccountRestoreJob extends BaseJob { @Override protected void onRun() throws Exception { SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey(); + StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey(); Log.i(TAG, "Retrieving manifest..."); Optional manifest = accountManager.getStorageManifest(storageServiceKey); @@ -76,8 +76,8 @@ public class StorageAccountRestoreJob extends BaseJob { return; } - Log.i(TAG, "Updating local manifest version to 0."); - TextSecurePreferences.setStorageManifestVersion(context, 0); + Log.i(TAG, "Resetting the local manifest to an empty state so that it will sync later."); + SignalStore.storageService().setManifest(SignalStorageManifest.EMPTY); Optional accountId = manifest.get().getAccountStorageId(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java index 130f8050a7..45a97af0f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -72,7 +72,7 @@ public class StorageForcePushJob extends BaseJob { @Override protected void onRun() throws IOException, RetryLaterException { - StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey(); + StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey(); SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); UnknownStorageIdDatabase storageIdDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context); @@ -117,7 +117,7 @@ public class StorageForcePushJob extends BaseJob { } Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion); - TextSecurePreferences.setStorageManifestVersion(context, newVersion); + SignalStore.storageService().setManifest(manifest); recipientDatabase.applyStorageIdUpdates(newContactStorageIds); recipientDatabase.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().getId(), accountRecord.getId())); storageIdDatabase.deleteAll(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java index a5ef11a9cb..947e11b114 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.storage.GroupV2RecordProcessor; import org.thoughtcrime.securesms.storage.StorageRecordUpdate; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult; -import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult; import org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult; import org.thoughtcrime.securesms.storage.StorageSyncModels; import org.thoughtcrime.securesms.storage.StorageSyncValidations; @@ -52,6 +51,7 @@ 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 org.whispersystems.signalservice.internal.storage.protos.SignalStorage; import java.io.IOException; import java.util.ArrayList; @@ -105,10 +105,9 @@ import java.util.concurrent.TimeUnit; * 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. + * - Finally, we assume that our local state represents the most up-to-date information, and so we + * calculate and write a change set that represents the diff between our state and the remote + * state. * * 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 @@ -130,8 +129,8 @@ import java.util.concurrent.TimeUnit; * - Update builder usage in StorageSyncModels * - Handle the new data when writing to the local storage * (i.e. {@link RecipientDatabase#applyStorageSyncContactUpdate(StorageRecordUpdate)}). - * - Make sure that whenever you change the field in the UI, we mark the row as dirty and call - * {@link StorageSyncHelper#scheduleSyncForDataChange()}. + * - Make sure that whenever you change the field in the UI, we rotate the storageId for that row + * and call {@link StorageSyncHelper#scheduleSyncForDataChange()}. * - If you're syncing a field that was otherwise already present in the UI, you'll probably want * to enqueue a {@link StorageServiceMigrationJob} as an app migration to make sure it gets * synced. @@ -185,7 +184,7 @@ public class StorageSyncJob extends BaseJob { ApplicationDependencies.getJobManager().add(new MultiDeviceStorageSyncRequestJob()); } - SignalStore.storageServiceValues().onSyncCompleted(); + SignalStore.storageService().onSyncCompleted(); } catch (InvalidKeyException e) { Log.w(TAG, "Failed to decrypt remote storage! Force-pushing and syncing the storage key to linked devices.", e); @@ -206,29 +205,28 @@ public class StorageSyncJob extends BaseJob { } private boolean performSync() throws IOException, RetryLaterException, InvalidKeyException { - Stopwatch stopwatch = new Stopwatch("StorageSync"); - Recipient self = Recipient.self().fresh(); - SQLiteDatabase db = DatabaseFactory.getInstance(context).getRawDatabase(); - SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); - RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); - UnknownStorageIdDatabase storageIdDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context); - StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageKey(); + final Stopwatch stopwatch = new Stopwatch("StorageSync"); + final SQLiteDatabase db = DatabaseFactory.getInstance(context).getRawDatabase(); + final SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + final UnknownStorageIdDatabase storageIdDatabase = DatabaseFactory.getUnknownStorageIdDatabase(context); + final StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey(); - boolean needsMultiDeviceSync = false; - boolean needsForcePush = false; - long localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context); - Optional remoteManifest = accountManager.getStorageManifestIfDifferentVersion(storageServiceKey, localManifestVersion); - long remoteManifestVersion = remoteManifest.transform(SignalStorageManifest::getVersion).or(localManifestVersion); + final SignalStorageManifest localManifest = SignalStore.storageService().getManifest(); + final SignalStorageManifest remoteManifest = accountManager.getStorageManifestIfDifferentVersion(storageServiceKey, localManifest.getVersion()).or(localManifest); + + Recipient self = Recipient.self().fresh(); + boolean needsMultiDeviceSync = false; + boolean needsForcePush = false; stopwatch.split("remote-manifest"); - Log.i(TAG, "Our version: " + localManifestVersion + ", their version: " + remoteManifestVersion); + Log.i(TAG, "Our version: " + localManifest.getVersion() + ", their version: " + remoteManifest.getVersion()); - if (remoteManifest.isPresent() && remoteManifestVersion > localManifestVersion) { + if (remoteManifest.getVersion() > localManifest.getVersion()) { Log.i(TAG, "[Remote Sync] Newer manifest version found!"); List localStorageIdsBeforeMerge = getAllLocalStorageIds(context, Recipient.self().fresh()); - IdDifferenceResult idDifference = StorageSyncHelper.findIdDifference(remoteManifest.get().getStorageIds(), localStorageIdsBeforeMerge); + IdDifferenceResult idDifference = StorageSyncHelper.findIdDifference(remoteManifest.getStorageIds(), localStorageIdsBeforeMerge); if (idDifference.hasTypeMismatches()) { Log.w(TAG, "[Remote Sync] Found type mismatches in the ID sets! Scheduling a force push after this sync completes."); @@ -247,8 +245,7 @@ public class StorageSyncJob extends BaseJob { stopwatch.split("remote-records"); if (remoteOnly.size() != idDifference.getRemoteOnlyIds().size()) { - Log.w(TAG, "[Remote Sync] Could not find all remote-only records! Requested: " + idDifference.getRemoteOnlyIds().size() + ", Found: " + remoteOnly.size() + ". Scheduling a force push after this sync completes."); - needsForcePush = true; + Log.w(TAG, "[Remote Sync] Could not find all remote-only records! Requested: " + idDifference.getRemoteOnlyIds().size() + ", Found: " + remoteOnly.size() + ". These stragglers should naturally get deleted during the sync."); } List remoteContacts = new LinkedList<>(); @@ -271,8 +268,6 @@ public class StorageSyncJob extends BaseJob { } } - WriteOperationResult mergeWriteOperation; - db.beginTransaction(); try { new ContactRecordProcessor(context, self).process(remoteContacts, StorageSyncHelper.KEY_GENERATOR); @@ -285,150 +280,73 @@ public class StorageSyncJob extends BaseJob { List unknownInserts = remoteUnknown; List unknownDeletes = Stream.of(idDifference.getLocalOnlyIds()).filter(StorageId::isUnknown).toList(); - storageIdDatabase.insert(unknownInserts); - storageIdDatabase.delete(unknownDeletes); - Log.i(TAG, "[Remote Sync] Unknowns :: " + unknownInserts.size() + " inserts, " + unknownDeletes.size() + " deletes"); - List localStorageIdsAfterMerge = getAllLocalStorageIds(context, Recipient.self().fresh()); - Set localIdsAdded = SetUtil.difference(localStorageIdsAfterMerge, localStorageIdsBeforeMerge); - Set localIdsRemoved = SetUtil.difference(localStorageIdsBeforeMerge, localStorageIdsAfterMerge); - - Log.i(TAG, "[Remote Sync] Local ID Changes :: " + localIdsAdded.size() + " inserts, " + localIdsRemoved.size() + " deletes"); - - IdDifferenceResult postMergeIdDifference = StorageSyncHelper.findIdDifference(remoteManifest.get().getStorageIds(), localStorageIdsAfterMerge); - List remoteInserts = buildLocalStorageRecords(context, self, postMergeIdDifference.getLocalOnlyIds()); - List remoteDeletes = Stream.of(postMergeIdDifference.getRemoteOnlyIds()).map(StorageId::getRaw).toList(); - - Log.i(TAG, "[Remote Sync] Post-Merge ID Difference :: " + postMergeIdDifference); - - mergeWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifestVersion + 1, localStorageIdsAfterMerge), - remoteInserts, - remoteDeletes); - - recipientDatabase.clearDirtyStateForStorageIds(Util.concatenatedList(localIdsAdded, localIdsRemoved)); + storageIdDatabase.insert(unknownInserts); + storageIdDatabase.delete(unknownDeletes); db.setTransactionSuccessful(); } finally { db.endTransaction(); ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners(); + stopwatch.split("remote-merge-transaction"); } - - stopwatch.split("remote-merge-transaction"); - - Log.i(TAG, "[Remote Sync] WriteOperationResult :: " + mergeWriteOperation); - - if (!mergeWriteOperation.isEmpty()) { - Log.i(TAG, "[Remote Sync] We have something to write remotely."); - - StorageSyncValidations.validate(mergeWriteOperation, remoteManifest, needsForcePush, self); - - Optional 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(); - } - - stopwatch.split("remote-merge-write"); - - remoteManifestVersion = mergeWriteOperation.getManifest().getVersion(); - remoteManifest = Optional.of(mergeWriteOperation.getManifest()); - - 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 IDs."); - Log.i(TAG, "[Remote Sync] Updating local manifest version to: " + remoteManifest.get().getVersion()); - TextSecurePreferences.setStorageManifestVersion(context, remoteManifest.get().getVersion()); + Log.i(TAG, "[Remote Sync] Remote version was newer, but there were no remote-only IDs."); } - } else if (remoteManifest.isPresent() && remoteManifestVersion < localManifestVersion) { - Log.w(TAG, "[Remote Sync] Remote version was older. User might have switched accounts. Making our version match."); - TextSecurePreferences.setStorageManifestVersion(context, remoteManifestVersion); + } else if (remoteManifest.getVersion() < localManifest.getVersion()) { + Log.w(TAG, "[Remote Sync] Remote version was older. User might have switched accounts."); } - localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context); + if (remoteManifest != localManifest) { + Log.i(TAG, "[Remote Sync] Saved new manifest. Now at version: " + remoteManifest.getVersion()); + SignalStore.storageService().setManifest(remoteManifest); + } - List allLocalStorageIds; - List pendingUpdates; - List pendingInsertions; - List pendingDeletions; - Optional pendingAccountInsert; - Optional pendingAccountUpdate; - Optional localWriteResult; + Log.i(TAG, "We are up-to-date with the remote storage state."); + + final WriteOperationResult remoteWriteOperation; db.beginTransaction(); try { - allLocalStorageIds = getAllLocalStorageIds(context, self); - pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates(); - pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions(); - pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions(); - pendingAccountInsert = StorageSyncHelper.getPendingAccountSyncInsert(context, self); - pendingAccountUpdate = StorageSyncHelper.getPendingAccountSyncUpdate(context, self); - localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion, - allLocalStorageIds, - pendingUpdates, - pendingInsertions, - pendingDeletions, - pendingAccountUpdate, - pendingAccountInsert); + List localStorageIds = getAllLocalStorageIds(context, Recipient.self().fresh()); + IdDifferenceResult idDifference = StorageSyncHelper.findIdDifference(remoteManifest.getStorageIds(), localStorageIds); + List remoteInserts = buildLocalStorageRecords(context, self, idDifference.getLocalOnlyIds()); + List remoteDeletes = Stream.of(idDifference.getRemoteOnlyIds()).map(StorageId::getRaw).toList(); + + Log.i(TAG, "ID Difference :: " + idDifference); + + remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1, localStorageIds), + remoteInserts, + remoteDeletes); + db.setTransactionSuccessful(); } finally { db.endTransaction(); + stopwatch.split("local-data-transaction"); } - stopwatch.split("local-changes-transaction"); + if (!remoteWriteOperation.isEmpty()) { + Log.i(TAG, "We have something to write remotely."); + Log.i(TAG, "WriteOperationResult :: " + remoteWriteOperation); - 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())); + StorageSyncValidations.validate(remoteWriteOperation, remoteManifest, needsForcePush, self); - WriteOperationResult localWrite = localWriteResult.get().getWriteResult(); - - Log.i(TAG, "[Local Changes] WriteOperationResult :: " + localWrite); - - if (localWrite.isEmpty()) { - throw new AssertionError("Decided there were local writes, but our write result was empty!"); - } - - StorageSyncValidations.validate(localWrite, Optional.absent(), needsForcePush, self); - - Optional conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes()); + Optional conflict = accountManager.writeStorageRecords(storageServiceKey, remoteWriteOperation.getManifest(), remoteWriteOperation.getInserts(), remoteWriteOperation.getDeletes()); if (conflict.isPresent()) { - Log.w(TAG, "[Local Changes] Hit a conflict when trying to upload our local writes! Retrying."); + Log.w(TAG, "Hit a conflict when trying to resolve the conflict! Retrying."); throw new RetryLaterException(); } - stopwatch.split("remote-change-write"); + Log.i(TAG, "Saved new manifest. Now at version: " + remoteWriteOperation.getManifest().getVersion()); + SignalStore.storageService().setManifest(remoteWriteOperation.getManifest()); - List clearIds = Util.concatenatedList(Stream.of(pendingUpdates).map(RecipientSettings::getId).toList(), - Stream.of(pendingInsertions).map(RecipientSettings::getId).toList(), - Stream.of(pendingDeletions).map(RecipientSettings::getId).toList(), - Collections.singletonList(Recipient.self().getId())); - - db.beginTransaction(); - try { - recipientDatabase.clearDirtyState(clearIds); - recipientDatabase.updateStorageIds(localWriteResult.get().getStorageKeyUpdates()); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - stopwatch.split("local-db-clean"); + stopwatch.split("remote-write"); 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."); + Log.i(TAG, "No remote writes needed. Still at version: " + remoteManifest.getVersion()); } if (needsForcePush) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java index 85c6d01fe5..5bbffadf61 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -62,7 +62,7 @@ public final class SignalStore { registrationValues().onFirstEverAppLaunch(); pinValues().onFirstEverAppLaunch(); remoteConfigValues().onFirstEverAppLaunch(); - storageServiceValues().onFirstEverAppLaunch(); + storageService().onFirstEverAppLaunch(); uiHints().onFirstEverAppLaunch(); tooltips().onFirstEverAppLaunch(); misc().onFirstEverAppLaunch(); @@ -83,7 +83,7 @@ public final class SignalStore { keys.addAll(registrationValues().getKeysToIncludeInBackup()); keys.addAll(pinValues().getKeysToIncludeInBackup()); keys.addAll(remoteConfigValues().getKeysToIncludeInBackup()); - keys.addAll(storageServiceValues().getKeysToIncludeInBackup()); + keys.addAll(storageService().getKeysToIncludeInBackup()); keys.addAll(uiHints().getKeysToIncludeInBackup()); keys.addAll(tooltips().getKeysToIncludeInBackup()); keys.addAll(misc().getKeysToIncludeInBackup()); @@ -124,7 +124,7 @@ public final class SignalStore { return INSTANCE.remoteConfigValues; } - public static @NonNull StorageServiceValues storageServiceValues() { + public static @NonNull StorageServiceValues storageService() { return INSTANCE.storageServiceValues; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java index 0848461e31..6a5984540a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java @@ -1,7 +1,9 @@ package org.thoughtcrime.securesms.keyvalue; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.StorageKey; import java.util.Collections; @@ -11,6 +13,7 @@ public class StorageServiceValues extends SignalStoreValues { private static final String LAST_SYNC_TIME = "storage.last_sync_time"; private static final String NEEDS_ACCOUNT_RESTORE = "storage.needs_account_restore"; + private static final String MANIFEST = "storage.manifest"; StorageServiceValues(@NonNull KeyValueStore store) { super(store); @@ -44,4 +47,18 @@ public class StorageServiceValues extends SignalStoreValues { public void setNeedsAccountRestore(boolean value) { putBoolean(NEEDS_ACCOUNT_RESTORE, value); } + + public void setManifest(@NonNull SignalStorageManifest manifest) { + putBlob(MANIFEST, manifest.serialize()); + } + + public @NonNull SignalStorageManifest getManifest() { + byte[] data = getBlob(MANIFEST, null); + + if (data != null) { + return SignalStorageManifest.deserialize(data); + } else { + return SignalStorageManifest.EMPTY; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPin.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPin.java index 97a69ac09a..2a7c139a28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPin.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionPin.java @@ -24,7 +24,7 @@ public class LogSectionPin implements LogSection { .append("Signal PIN: ").append(SignalStore.kbsValues().hasPin()).append("\n") .append("Opted Out: ").append(SignalStore.kbsValues().hasOptedOut()).append("\n") .append("Last Creation Failed: ").append(SignalStore.kbsValues().lastPinCreateFailed()).append("\n") - .append("Needs Account Restore: ").append(SignalStore.storageServiceValues().needsAccountRestore()).append("\n") + .append("Needs Account Restore: ").append(SignalStore.storageService().needsAccountRestore()).append("\n") .append("PIN Required at Registration: ").append(SignalStore.registrationValues().pinWasRequiredAtRegistration()).append("\n") .append("Registration Complete: ").append(SignalStore.registrationValues().isRegistrationComplete()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java index e2e6e1b890..0efa190e04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinRestoreViewModel.java @@ -51,7 +51,7 @@ public class PinRestoreViewModel extends ViewModel { switch (result.getResult()) { case SUCCESS: SignalStore.pinValues().setKeyboardType(pinKeyboardType); - SignalStore.storageServiceValues().setNeedsAccountRestore(false); + SignalStore.storageService().setNeedsAccountRestore(false); event.postValue(Event.SUCCESS); break; case LOCKED: diff --git a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java index 43231e7e1a..84cb98d831 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/pin/PinState.java @@ -126,7 +126,7 @@ public final class PinState { Log.i(TAG, "Has a PIN to restore."); SignalStore.kbsValues().clearRegistrationLockAndPin(); TextSecurePreferences.setV1RegistrationLockEnabled(context, false); - SignalStore.storageServiceValues().setNeedsAccountRestore(true); + SignalStore.storageService().setNeedsAccountRestore(true); } else { Log.i(TAG, "No registration lock or PIN at all."); SignalStore.kbsValues().clearRegistrationLockAndPin(); @@ -145,7 +145,7 @@ public final class PinState { SignalStore.kbsValues().setKbsMasterKey(kbsData, pin); SignalStore.kbsValues().setV2RegistrationLockEnabled(false); SignalStore.pinValues().resetPinReminders(); - SignalStore.storageServiceValues().setNeedsAccountRestore(false); + SignalStore.storageService().setNeedsAccountRestore(false); resetPinRetryCount(context, pin); ClearFallbackKbsEnclaveJob.clearAll(); @@ -157,7 +157,7 @@ public final class PinState { */ public static synchronized void onPinRestoreForgottenOrSkipped() { SignalStore.kbsValues().clearRegistrationLockAndPin(); - SignalStore.storageServiceValues().setNeedsAccountRestore(false); + SignalStore.storageService().setNeedsAccountRestore(false); updateState(buildInferredStateFromOtherFields()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java index 17fc98a524..5fc422c5b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java @@ -32,7 +32,7 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment FragmentActivity activity = requireActivity(); - if (SignalStore.storageServiceValues().needsAccountRestore()) { + if (SignalStore.storageService().needsAccountRestore()) { activity.startActivity(new Intent(activity, PinRestoreActivity.class)); } else if (!isReregister()) { final Intent main = MainActivity.clearTop(activity); diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java index ba4b87e584..e0255a7b29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java @@ -57,123 +57,6 @@ public final class StorageSyncHelper { private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(2); - /** - * Given the local state of pending storage mutations, this will generate a result that will - * include that data that needs to be written to the storage service, as well as any changes you - * need to write back to local storage (like storage keys that might have changed for updated - * contacts). - * - * @param currentManifestVersion What you think the version is locally. - * @param currentLocalKeys All local keys you have. This assumes that 'inserts' were given keys - * already, and that deletes still have keys. - * @param updates Contacts that have been altered. - * @param inserts Contacts that have been inserted (or newly marked as registered). - * @param deletes Contacts that are no longer registered. - * - * @return If changes need to be written, then it will return those changes. If no changes need - * to be written, this will return {@link Optional#absent()}. - */ - public static @NonNull Optional buildStorageUpdatesForLocal(long currentManifestVersion, - @NonNull List currentLocalKeys, - @NonNull List updates, - @NonNull List inserts, - @NonNull List deletes, - @NonNull Optional accountUpdate, - @NonNull Optional accountInsert) - { - int accountCount = Stream.of(currentLocalKeys) - .filter(id -> id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE) - .toList() - .size(); - - if (accountCount > 1) { - throw new MultipleExistingAccountsException(); - } - - Optional accountId = Optional.fromNullable(Stream.of(currentLocalKeys) - .filter(id -> id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE) - .findFirst() - .orElse(null)); - - - if (accountId.isPresent() && accountInsert.isPresent() && !accountInsert.get().getId().equals(accountId.get())) { - throw new InvalidAccountInsertException(); - } - - if (accountId.isPresent() && accountUpdate.isPresent() && !accountUpdate.get().getId().equals(accountId.get())) { - throw new InvalidAccountUpdateException(); - } - - if (accountUpdate.isPresent() && accountInsert.isPresent()) { - throw new InvalidAccountDualInsertUpdateException(); - } - - Set completeIds = new LinkedHashSet<>(currentLocalKeys); - Set storageInserts = new LinkedHashSet<>(); - Set storageDeletes = new LinkedHashSet<>(); - Map storageKeyUpdates = new HashMap<>(); - - for (RecipientSettings insert : inserts) { - if (insert.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && insert.getSyncExtras().getGroupMasterKey() == null) { - Log.w(TAG, "Missing master key on gv2 recipient"); - continue; - } - - SignalStorageRecord insertRecord = StorageSyncModels.localToRemoteRecord(insert); - storageInserts.add(insertRecord); - completeIds.add(insertRecord.getId()); - } - - if (accountInsert.isPresent()) { - storageInserts.add(SignalStorageRecord.forAccount(accountInsert.get())); - completeIds.add(accountInsert.get().getId()); - } - - for (RecipientSettings delete : deletes) { - byte[] key = Objects.requireNonNull(delete.getStorageId()); - storageDeletes.add(ByteBuffer.wrap(key)); - completeIds.removeIf(id -> Arrays.equals(id.getRaw(), key)); - } - - for (RecipientSettings update : updates) { - byte[] oldId = update.getStorageId(); - byte[] newId = generateKey(); - - SignalStorageRecord insert = StorageSyncModels.localToRemoteRecord(update, newId); - - storageInserts.add(insert); - storageDeletes.add(ByteBuffer.wrap(oldId)); - - completeIds.add(insert.getId()); - completeIds.removeIf(id -> Arrays.equals(id.getRaw(), oldId)); - - storageKeyUpdates.put(update.getId(), newId); - } - - if (accountUpdate.isPresent()) { - StorageId oldId = accountUpdate.get().getId(); - StorageId newId = StorageId.forAccount(generateKey()); - - storageInserts.add(SignalStorageRecord.forAccount(newId, accountUpdate.get())); - storageDeletes.add(ByteBuffer.wrap(oldId.getRaw())); - - completeIds.remove(oldId); - completeIds.add(newId); - - storageKeyUpdates.put(Recipient.self().getId(), newId.getRaw()); - } - - if (storageInserts.isEmpty() && storageDeletes.isEmpty()) { - return Optional.absent(); - } else { - List storageDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList(); - SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, new ArrayList<>(completeIds)); - WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), storageDeleteBytes); - - return Optional.of(new LocalWriteResult(writeOperationResult, storageKeyUpdates)); - } - } - /** * Given a list of all the local and remote keys you know about, this will return a result telling * you which keys are exclusively remote and which are exclusively local. @@ -227,20 +110,6 @@ public final class StorageSyncHelper { return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey()); } - public static Optional getPendingAccountSyncUpdate(@NonNull Context context, @NonNull Recipient self) { - if (DatabaseFactory.getRecipientDatabase(context).getDirtyState(self.getId()) != RecipientDatabase.DirtyState.UPDATE) { - return Optional.absent(); - } - return Optional.of(buildAccountRecord(context, self).getAccount().get()); - } - - public static Optional getPendingAccountSyncInsert(@NonNull Context context, @NonNull Recipient self) { - if (DatabaseFactory.getRecipientDatabase(context).getDirtyState(self.getId()) != RecipientDatabase.DirtyState.INSERT) { - return Optional.absent(); - } - return Optional.of(buildAccountRecord(context, self).getAccount().get()); - } - public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @NonNull Recipient self) { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); RecipientSettings settings = recipientDatabase.getRecipientSettingsForSync(self.getId()); @@ -296,7 +165,7 @@ public final class StorageSyncHelper { } public static void scheduleRoutineSync() { - long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageServiceValues().getLastSyncTime(); + long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageService().getLastSyncTime(); if (timeSinceLastSync > REFRESH_INTERVAL) { Log.d(TAG, "Scheduling a sync. Last sync was " + timeSinceLastSync + " ms ago."); @@ -390,27 +259,4 @@ public final class StorageSyncHelper { } } } - - public static class LocalWriteResult { - private final WriteOperationResult writeResult; - private final Map storageKeyUpdates; - - private LocalWriteResult(WriteOperationResult writeResult, Map storageKeyUpdates) { - this.writeResult = writeResult; - this.storageKeyUpdates = storageKeyUpdates; - } - - public @NonNull WriteOperationResult getWriteResult() { - return writeResult; - } - - public @NonNull Map getStorageKeyUpdates() { - return storageKeyUpdates; - } - } - - private static final class MultipleExistingAccountsException extends IllegalArgumentException {} - private static final class InvalidAccountInsertException extends IllegalArgumentException {} - private static final class InvalidAccountUpdateException extends IllegalArgumentException {} - private static final class InvalidAccountDualInsertUpdateException extends IllegalArgumentException {} } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java index 6e4339499e..84565b4975 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java @@ -30,7 +30,7 @@ public final class StorageSyncValidations { private StorageSyncValidations() {} public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result, - @NonNull Optional previousManifest, + @NonNull SignalStorageManifest previousManifest, boolean forcePushPending, @NonNull Recipient self) { @@ -47,12 +47,12 @@ public final class StorageSyncValidations { } } - if (!previousManifest.isPresent()) { - Log.i(TAG, "No previous manifest, not bothering with additional validations around the diffs between the two manifests."); + if (previousManifest.getVersion() == 0) { + Log.i(TAG, "Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests."); return; } - if (result.getManifest().getVersion() != previousManifest.get().getVersion() + 1) { + if (result.getManifest().getVersion() != previousManifest.getVersion() + 1) { throw new IncorrectManifestVersionError(); } @@ -61,7 +61,7 @@ public final class StorageSyncValidations { return; } - Set previousIds = Stream.of(previousManifest.get().getStorageIds()).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet()); + Set previousIds = Stream.of(previousManifest.getStorageIds()).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet()); Set newIds = Stream.of(result.getManifest().getStorageIds()).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet()); Set manifestInserts = SetUtil.difference(newIds, previousIds); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 36a54eb9cc..8e59afa637 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -1224,10 +1224,6 @@ public class TextSecurePreferences { setBooleanPreference(context, HAS_SEEN_VIDEO_RECORDING_TOOLTIP, value); } - public static long getStorageManifestVersion(Context context) { - return getLongPreference(context, STORAGE_MANIFEST_VERSION, 0); - } - public static void setStorageManifestVersion(Context context, long version) { setLongPreference(context, STORAGE_MANIFEST_VERSION, version); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageManifest.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageManifest.java index 81c102d234..ab7aaf5be3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageManifest.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageManifest.java @@ -1,16 +1,22 @@ package org.whispersystems.signalservice.api.storage; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; + import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import org.whispersystems.signalservice.internal.storage.protos.StorageManifest; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; public class SignalStorageManifest { + public static final SignalStorageManifest EMPTY = new SignalStorageManifest(0, Collections.emptyList()); + private final long version; private final List storageIds; private final Map> storageIdsByType; @@ -30,6 +36,22 @@ public class SignalStorageManifest { } } + public static SignalStorageManifest deserialize(byte[] serialized) { + try { + StorageManifest manifest = StorageManifest.parseFrom(serialized); + ManifestRecord manifestRecord = ManifestRecord.parseFrom(manifest.getValue()); + List ids = new ArrayList<>(manifestRecord.getIdentifiersCount()); + + for (ManifestRecord.Identifier id : manifestRecord.getIdentifiersList()) { + ids.add(StorageId.forType(id.getRaw().toByteArray(), id.getTypeValue())); + } + + return new SignalStorageManifest(manifest.getVersion(), ids); + } catch (InvalidProtocolBufferException e) { + throw new AssertionError(e); + } + } + public long getVersion() { return version; } @@ -47,4 +69,23 @@ public class SignalStorageManifest { return Optional.absent(); } } + + public byte[] serialize() { + List ids = new ArrayList<>(storageIds.size()); + + for (StorageId id : storageIds) { + ids.add(ManifestRecord.Identifier.newBuilder() + .setTypeValue(id.getType()) + .setRaw(ByteString.copyFrom(id.getRaw())) + .build()); + } + + ManifestRecord manifestRecord = ManifestRecord.newBuilder().addAllIdentifiers(ids).build(); + + return StorageManifest.newBuilder() + .setVersion(version) + .setValue(manifestRecord.toByteString()) + .build() + .toByteArray(); + } }