From 951a61117a0c11fd1ee72ec6aee1544a696fd3b1 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 18 Mar 2020 16:31:45 -0400 Subject: [PATCH] Add storage support for the AccountRecord. --- .../securesms/ApplicationContext.java | 3 +- .../securesms/VerifyIdentityActivity.java | 3 +- .../contacts/sync/DirectoryHelper.java | 10 +- .../securesms/database/RecipientDatabase.java | 119 +++++++-- .../securesms/database/ThreadDatabase.java | 54 +++- .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/jobs/ProfileUploadJob.java | 7 + .../securesms/jobs/PushProcessMessageJob.java | 3 +- .../jobs/StorageAccountRestoreJob.java | 127 +++++++++ .../securesms/jobs/StorageForcePushJob.java | 21 +- .../securesms/jobs/StorageSyncJob.java | 109 +++++--- .../securesms/keyvalue/SignalStore.java | 1 - .../keyvalue/StorageServiceValues.java | 13 +- .../AppProtectionPreferenceFragment.java | 69 +++-- .../securesms/recipients/Recipient.java | 10 +- .../recipients/RecipientDetails.java | 6 +- .../securesms/recipients/RecipientUtil.java | 6 +- .../fragments/RegistrationLockFragment.java | 8 +- .../storage/AccountConflictMerger.java | 116 ++++++++ .../storage/ContactConflictMerger.java | 25 +- .../storage/GroupV1ConflictMerger.java | 6 + .../storage/GroupV2ConflictMerger.java | 6 + .../securesms/storage/StorageSyncHelper.java | 252 ++++++++++++++---- .../securesms/storage/StorageSyncModels.java | 21 +- .../storage/StorageSyncValidations.java | 93 +++++++ .../org/thoughtcrime/securesms/util/Util.java | 11 +- .../storage/ContactConflictMergerTest.java | 46 +++- .../storage/StorageSyncHelperTest.java | 169 ++++++++---- .../api/SignalServiceAccountManager.java | 16 ++ .../api/storage/SignalAccountRecord.java | 148 ++++++++++ .../api/storage/SignalContactRecord.java | 2 +- .../api/storage/SignalGroupV1Record.java | 2 +- .../api/storage/SignalGroupV2Record.java | 2 +- .../api/storage/SignalStorageManifest.java | 37 ++- .../api/storage/SignalStorageModels.java | 53 +--- .../api/storage/SignalStorageRecord.java | 28 +- .../signalservice/api/storage/StorageId.java | 4 + .../src/main/proto/SignalStorage.proto | 18 +- 38 files changed, 1290 insertions(+), 335 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 29056202d3..8c8ba0167d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -71,6 +71,7 @@ import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager; import org.thoughtcrime.securesms.service.RotateSenderCertificateListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; @@ -136,7 +137,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi FeatureFlags.init(); NotificationChannels.create(this); RefreshPreKeysJob.scheduleIfNecessary(); - StorageSyncJob.scheduleIfNecessary(); + StorageSyncHelper.scheduleRoutineSync(); ProcessLifecycleOwner.get().getLifecycle().addObserver(this); if (Build.VERSION.SDK_INT < 21) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/app/src/main/java/org/thoughtcrime/securesms/VerifyIdentityActivity.java index 1f4acd054b..ec304aaaa4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/VerifyIdentityActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/VerifyIdentityActivity.java @@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.qr.ScanningThread; import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme; import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; @@ -605,7 +606,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity remoteIdentity, isChecked ? VerifiedStatus.VERIFIED : VerifiedStatus.DEFAULT)); - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), isChecked, false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java index d79d088857..020cb571d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/sync/DirectoryHelper.java @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.FeatureFlags; import java.io.IOException; @@ -21,11 +22,6 @@ public class DirectoryHelper { @WorkerThread public static void refreshDirectory(@NonNull Context context, boolean notifyOfNewUsers) throws IOException { - if (!SignalStore.storageServiceValues().hasFirstStorageSyncCompleted()) { - Log.i(TAG, "First storage sync has not completed. Skipping."); - return; - } - if (FeatureFlags.uuids()) { // TODO [greyson] Create a DirectoryHelperV2 when appropriate. DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers); @@ -33,7 +29,7 @@ public class DirectoryHelper { DirectoryHelperV1.refreshDirectory(context, notifyOfNewUsers); } - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); } @WorkerThread @@ -49,7 +45,7 @@ public class DirectoryHelper { } if (newRegisteredState != originalRegisteredState) { - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); } return newRegisteredState; 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 241aa5e5ce..d8c6c5aa41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -39,6 +39,7 @@ import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.guava.Optional; 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.StorageId; @@ -216,7 +217,7 @@ public class RecipientDatabase extends Database { } } - enum DirtyState { + public enum DirtyState { CLEAN(0), UPDATE(1), INSERT(2), DELETE(3); private final int id; @@ -228,6 +229,10 @@ public class RecipientDatabase extends Database { int getId() { return id; } + + public static DirtyState fromId(int id) { + return values()[id]; + } } public enum GroupType { @@ -401,23 +406,35 @@ 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 @NonNull List getPendingRecipientSyncUpdates() { - String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL"; - String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()) }; + String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; + String[] args = new String[] { String.valueOf(DirtyState.UPDATE.getId()), Recipient.self().getId().serialize() }; return getRecipientSettings(query, args); } public @NonNull List getPendingRecipientSyncInsertions() { - String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL"; - String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()) }; + String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; + String[] args = new String[] { String.valueOf(DirtyState.INSERT.getId()), Recipient.self().getId().serialize() }; return getRecipientSettings(query, args); } public @NonNull List getPendingRecipientSyncDeletions() { - String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL"; - String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()) }; + String query = DIRTY + " = ? AND " + STORAGE_SERVICE_ID + " NOT NULL AND " + TABLE_NAME + "." + ID + " != ?"; + String[] args = new String[] { String.valueOf(DirtyState.DELETE.getId()), Recipient.self().getId().serialize() }; return getRecipientSettings(query, args); } @@ -432,6 +449,10 @@ public class RecipientDatabase extends Database { return null; } + public void markNeedsSync(@NonNull RecipientId recipientId) { + markDirty(recipientId, DirtyState.UPDATE); + } + public void applyStorageIdUpdates(@NonNull Map storageIds) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); @@ -459,6 +480,7 @@ public class RecipientDatabase extends Database { { SQLiteDatabase db = databaseHelper.getWritableDatabase(); IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context); + ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context); db.beginTransaction(); @@ -492,9 +514,8 @@ public class RecipientDatabase extends Database { } } - if (Recipient.self().getId().equals(recipientId)) { - TextSecurePreferences.setProfileName(context, ProfileName.fromParts(insert.getGivenName().orNull(), insert.getFamilyName().orNull())); - } + threadDatabase.setArchived(recipientId, insert.isArchived()); + Recipient.live(recipientId).refresh(); } } @@ -534,10 +555,18 @@ public class RecipientDatabase extends Database { } catch (InvalidKeyException e) { Log.w(TAG, "Failed to process identity key during update! Skipping.", e); } + + threadDatabase.setArchived(recipientId, update.getNew().isArchived()); + Recipient.live(recipientId).refresh(); } for (SignalGroupV1Record insert : groupV1Inserts) { db.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert)); + + Recipient recipient = Recipient.externalGroup(context, GroupUtil.getEncodedId(insert.getGroupId(), false)); + + threadDatabase.setArchived(recipient.getId(), insert.isArchived()); + recipient.live().refresh(); } for (RecordUpdate update : groupV1Updates) { @@ -547,6 +576,11 @@ public class RecipientDatabase extends Database { if (updateCount < 1) { throw new AssertionError("Had an update, but it didn't match any rows!"); } + + Recipient recipient = Recipient.externalGroup(context, GroupUtil.getEncodedId(update.getOld().getGroupId(), false)); + + threadDatabase.setArchived(recipient.getId(), update.getNew().isArchived()); + recipient.live().refresh(); } db.setTransactionSuccessful(); @@ -555,6 +589,27 @@ public class RecipientDatabase extends Database { } } + public void applyStorageSyncUpdates(@NonNull StorageId storageId, SignalAccountRecord update) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + ContentValues values = new ContentValues(); + ProfileName profileName = ProfileName.fromParts(update.getGivenName().orNull(), update.getFamilyName().orNull()); + + values.put(PROFILE_GIVEN_NAME, profileName.getGivenName()); + values.put(PROFILE_FAMILY_NAME, profileName.getFamilyName()); + values.put(PROFILE_JOINED_NAME, profileName.toString()); + values.put(PROFILE_KEY, update.getProfileKey().transform(Base64::encodeBytes).orNull()); + values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.getId().getRaw())); + values.put(DIRTY, DirtyState.CLEAN.getId()); + + int updateCount = db.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", new String[]{Base64.encodeBytes(storageId.getRaw())}); + if (updateCount < 1) { + throw new AssertionError("Account update didn't match any rows!"); + } + + Recipient.self().live().refresh(); + } + public void updatePhoneNumbers(@NonNull Map mapping) { if (mapping.isEmpty()) return; @@ -641,19 +696,19 @@ public class RecipientDatabase extends Database { } /** - * @return All storage keys, excluding the ones that need to be deleted. + * @return All storage ids for ContactRecords, excluding the ones that need to be deleted. */ - public List getAllStorageSyncKeys() { - return new ArrayList<>(getAllStorageSyncKeysMap().values()); + public List getContactStorageSyncIds() { + return new ArrayList<>(getContactStorageSyncIdsMap().values()); } /** - * @return All storage keys, excluding the ones that need to be deleted. + * @return All storage IDs for ContactRecords, excluding the ones that need to be deleted. */ - public @NonNull Map getAllStorageSyncKeysMap() { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ?"; - String[] args = new String[]{String.valueOf(DirtyState.DELETE)}; + public @NonNull Map getContactStorageSyncIdsMap() { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = STORAGE_SERVICE_ID + " NOT NULL AND " + DIRTY + " != ? AND " + ID + " != ?"; + String[] args = new String[]{String.valueOf(DirtyState.DELETE), Recipient.self().getId().serialize() }; Map out = new HashMap<>(); try (Cursor cursor = db.query(TABLE_NAME, new String[] { ID, STORAGE_SERVICE_ID, GROUP_TYPE }, query, args, null, null, null)) { @@ -920,7 +975,7 @@ public class RecipientDatabase extends Database { if (update(updateQuery, valuesToSet)) { markDirty(id, DirtyState.UPDATE); Recipient.live(id).refresh(); - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); return true; } else { return false; @@ -966,7 +1021,7 @@ public class RecipientDatabase extends Database { if (update(id, contentValues)) { markDirty(id, DirtyState.UPDATE); Recipient.live(id).refresh(); - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); } } @@ -975,6 +1030,11 @@ public class RecipientDatabase extends Database { contentValues.put(SIGNAL_PROFILE_AVATAR, profileAvatar); if (update(id, contentValues)) { Recipient.live(id).refresh(); + + if (id.equals(Recipient.self().getId())) { + markDirty(id, DirtyState.UPDATE); + StorageSyncHelper.scheduleSyncForDataChange(); + } } } @@ -984,7 +1044,7 @@ public class RecipientDatabase extends Database { if (update(id, contentValues)) { markDirty(id, DirtyState.UPDATE); Recipient.live(id).refresh(); - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); } } @@ -1002,6 +1062,7 @@ public class RecipientDatabase extends Database { if (update(id, contentValues)) { markDirty(id, DirtyState.UPDATE); Recipient.live(id).refresh(); + StorageSyncHelper.scheduleSyncForDataChange(); } } @@ -1017,8 +1078,10 @@ public class RecipientDatabase extends Database { ContentValues contentValues = new ContentValues(1); contentValues.put(USERNAME, username); - update(id, contentValues); - Recipient.live(id).refresh(); + if (update(id, contentValues)) { + Recipient.live(id).refresh(); + StorageSyncHelper.scheduleSyncForDataChange(); + } } public void clearUsernameIfExists(@NonNull String username) { @@ -1601,7 +1664,7 @@ public class RecipientDatabase extends Database { private final Recipient.Capability uuidCapability; private final Recipient.Capability groupsV2Capability; private final InsightsBannerTier insightsBannerTier; - private final byte[] storageKey; + private final byte[] storageId; private final byte[] identityKey; private final IdentityDatabase.VerifiedStatus identityStatus; @@ -1637,7 +1700,7 @@ public class RecipientDatabase extends Database { Recipient.Capability uuidCapability, Recipient.Capability groupsV2Capability, @NonNull InsightsBannerTier insightsBannerTier, - @Nullable byte[] storageKey, + @Nullable byte[] storageId, @Nullable byte[] identityKey, @NonNull IdentityDatabase.VerifiedStatus identityStatus) { @@ -1673,7 +1736,7 @@ public class RecipientDatabase extends Database { this.uuidCapability = uuidCapability; this.groupsV2Capability = groupsV2Capability; this.insightsBannerTier = insightsBannerTier; - this.storageKey = storageKey; + this.storageId = storageId; this.identityKey = identityKey; this.identityStatus = identityStatus; } @@ -1806,8 +1869,8 @@ public class RecipientDatabase extends Database { return groupsV2Capability; } - public @Nullable byte[] getStorageKey() { - return storageKey; + public @Nullable byte[] getStorageId() { + return storageId; } public @Nullable byte[] getIdentityKey() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index 1091bdd221..4dee70da16 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; @@ -53,6 +54,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import java.io.Closeable; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -422,10 +424,48 @@ public class ThreadDatabase extends Database { return getConversationList("1"); } + public boolean isArchived(@NonNull RecipientId recipientId) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = RECIPIENT_ID + " = ?"; + String[] args = new String[]{ recipientId.serialize() }; + + try (Cursor cursor = db.query(TABLE_NAME, new String[] { ARCHIVED }, query, args, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(cursor.getColumnIndexOrThrow(ARCHIVED)) == 1; + } + } + + return false; + } + + public void setArchived(@NonNull RecipientId recipientId, boolean status) { + setArchived(Collections.singletonMap(recipientId, status)); + } + + public void setArchived(@NonNull Map status) { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + + db.beginTransaction(); + try { + String query = RECIPIENT_ID + " = ?"; + + for (Map.Entry entry : status.entrySet()) { + ContentValues values = new ContentValues(1); + values.put(ARCHIVED, entry.getValue() ? "1" : "0"); + db.update(TABLE_NAME, values, query, new String[] { entry.getKey().serialize() }); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + notifyConversationListListeners(); + } + } + public @NonNull Set getArchivedRecipients() { Set archived = new HashSet<>(); - try (Cursor cursor = DatabaseFactory.getThreadDatabase(context).getArchivedConversationList()) { + try (Cursor cursor = getArchivedConversationList()) { while (cursor != null && cursor.moveToNext()) { archived.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.RECIPIENT_ID)))); } @@ -488,6 +528,12 @@ public class ThreadDatabase extends Database { db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""}); notifyConversationListListeners(); + + Recipient recipient = getRecipientForThreadId(threadId); + if (recipient != null) { + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipient.getId()); + StorageSyncHelper.scheduleSyncForDataChange(); + } } public void unarchiveConversation(long threadId) { @@ -497,6 +543,12 @@ public class ThreadDatabase extends Database { db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId + ""}); notifyConversationListListeners(); + + Recipient recipient = getRecipientForThreadId(threadId); + if (recipient != null) { + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(recipient.getId()); + StorageSyncHelper.scheduleSyncForDataChange(); + } } public void setLastSeen(long threadId) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 0a172bf002..149a7a9a1c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -85,6 +85,7 @@ public final class JobManagerFactories { put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory()); put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory()); put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory()); + put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory()); put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory()); put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory()); put(RotateCertificateJob.KEY, new RotateCertificateJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java index f3dc869670..043a1f997a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ProfileUploadJob.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import org.signal.zkgroup.profiles.ProfileKey; import org.thoughtcrime.securesms.crypto.ProfileKeyUtil; +import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; @@ -14,8 +15,11 @@ import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.ProfileName; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.FeatureFlags; +import org.thoughtcrime.securesms.util.ProfileUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; +import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.util.StreamDetails; public final class ProfileUploadJob extends BaseJob { @@ -55,6 +59,9 @@ public final class ProfileUploadJob extends BaseJob { accountManager.setProfileAvatar(profileKey, avatar); } } + + ProfileAndCredential profile = ProfileUtil.retrieveProfile(context, Recipient.self(), SignalServiceProfile.RequestType.PROFILE); + DatabaseFactory.getRecipientDatabase(context).setProfileAvatar(Recipient.self().getId(), profile.getProfile().getAvatar()); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java index b4ef608c3d..84c240b175 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushProcessMessageJob.java @@ -72,6 +72,7 @@ import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage; import org.thoughtcrime.securesms.sms.OutgoingEndSessionMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.stickers.StickerLocator; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Hex; @@ -665,7 +666,7 @@ public final class PushProcessMessageJob extends BaseJob { ApplicationDependencies.getJobManager().add(new RefreshOwnProfileJob()); break; case STORAGE_MANIFEST: - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); break; default: Log.w(TAG, "Received a fetch message for an unknown type."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java new file mode 100644 index 0000000000..6950f85d41 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageAccountRestoreJob.java @@ -0,0 +1,127 @@ +package org.thoughtcrime.securesms.jobs; + +import androidx.annotation.NonNull; + +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.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; +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.SignalStorageManifest; +import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.api.storage.StorageId; +import org.whispersystems.signalservice.api.storage.StorageKey; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Restored the AccountRecord present in the storage service, if any. This will overwrite any local + * data that is stored in AccountRecord, so this should only be done immediately after registration. + */ +public class StorageAccountRestoreJob extends BaseJob { + + public static String KEY = "StorageAccountRestoreJob"; + + public static long LIFESPAN = TimeUnit.SECONDS.toMillis(10); + + private static final String TAG = Log.tag(StorageAccountRestoreJob.class); + + public StorageAccountRestoreJob() { + this(new Parameters.Builder() + .setQueue(StorageSyncJob.QUEUE_KEY) + .addConstraint(NetworkConstraint.KEY) + .setMaxInstances(1) + .setMaxAttempts(1) + .setLifespan(LIFESPAN) + .build()); + } + + private StorageAccountRestoreJob(@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 Exception { + SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager(); + StorageKey storageServiceKey = SignalStore.storageServiceValues().getOrCreateStorageMasterKey().deriveStorageServiceKey(); + + Optional manifest = accountManager.getStorageManifest(storageServiceKey); + + if (!manifest.isPresent()) { + Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring."); + return; + } + + Optional accountId = manifest.get().getAccountStorageId(); + + if (!accountId.isPresent()) { + Log.w(TAG, "Manifest had no account record! Not restoring."); + return; + } + + List records = accountManager.readStorageRecords(storageServiceKey, Collections.singletonList(accountId.get())); + SignalStorageRecord record = records.size() > 0 ? records.get(0) : null; + + if (record == null) { + Log.w(TAG, "Could not find account record, even though we had an ID! Not restoring."); + return; + } + + SignalAccountRecord accountRecord = record.getAccount().orNull(); + if (accountRecord == null) { + Log.w(TAG, "The storage record didn't actually have an account on it! Not restoring."); + return; + } + + StorageId selfStorageId = StorageId.forAccount(Recipient.self().getStorageServiceId()); + StorageSyncHelper.applyAccountStorageSyncUpdates(context, selfStorageId, accountRecord); + + if (accountRecord.getAvatarUrlPath().isPresent()) { + RetrieveProfileAvatarJob avatarJob = new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get()); + try { + avatarJob.setContext(context); + avatarJob.onRun(); + } catch (IOException e) { + Log.w(TAG, "Failed to download avatar. Scheduling for later."); + ApplicationDependencies.getJobManager().add(avatarJob); + } + } + } + + @Override + protected boolean onShouldRetry(@NonNull Exception e) { + return e instanceof PushNetworkException; + } + + @Override + public void onFailure() { + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull + StorageAccountRestoreJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new StorageAccountRestoreJob(parameters); + } + } +} 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 2b8dbe81c7..19d4f40c0b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java @@ -4,6 +4,8 @@ import androidx.annotation.NonNull; import com.annimon.stream.Stream; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncModels; import org.thoughtcrime.securesms.database.DatabaseFactory; @@ -34,6 +36,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -76,15 +79,17 @@ public class StorageForcePushJob extends BaseJob { StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context); long currentVersion = accountManager.getStorageManifestVersion(); - Map oldStorageKeys = recipientDatabase.getAllStorageSyncKeysMap(); + Map oldStorageKeys = recipientDatabase.getContactStorageSyncIdsMap(); - long newVersion = currentVersion + 1; - Map newStorageKeys = generateNewKeys(oldStorageKeys); - List inserts = Stream.of(oldStorageKeys.keySet()) - .map(recipientDatabase::getRecipientSettings) - .withoutNulls() - .map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId())).getRaw())) - .toList(); + long newVersion = currentVersion + 1; + Map newStorageKeys = generateNewKeys(oldStorageKeys); + Set archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients(); + List inserts = Stream.of(oldStorageKeys.keySet()) + .map(recipientDatabase::getRecipientSettings) + .withoutNulls() + .map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newStorageKeys.get(s.getId())).getRaw(), archivedRecipients)) + .toList(); + inserts.add(StorageSyncHelper.buildAccountRecord(context, StorageId.forAccount(Recipient.self().fresh().getStorageServiceId()))); SignalStorageManifest manifest = new SignalStorageManifest(newVersion, new ArrayList<>(newStorageKeys.values())); 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 9f2750f449..219aa071f3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageSyncJob.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import com.annimon.stream.Stream; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult; import org.thoughtcrime.securesms.storage.StorageSyncHelper.LocalWriteResult; @@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.storage.StorageSyncValidations; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -30,16 +32,20 @@ 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.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -56,27 +62,14 @@ public class StorageSyncJob extends BaseJob { private static final String TAG = Log.tag(StorageSyncJob.class); - private static final long REFRESH_INTERVAL = TimeUnit.HOURS.toMillis(2); - public StorageSyncJob() { this(new Job.Parameters.Builder().addConstraint(NetworkConstraint.KEY) .setQueue(QUEUE_KEY) - .setMaxInstances(1) + .setMaxInstances(2) .setLifespan(TimeUnit.DAYS.toMillis(1)) .build()); } - public static void scheduleIfNecessary() { - long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageServiceValues().getLastSyncTime(); - - if (timeSinceLastSync > REFRESH_INTERVAL) { - Log.d(TAG, "Scheduling a sync. Last sync was " + timeSinceLastSync + " ms ago."); - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); - } else { - Log.d(TAG, "No need for sync. Last sync was " + timeSinceLastSync + " ms ago."); - } - } - private StorageSyncJob(@NonNull Parameters parameters) { super(parameters); } @@ -98,6 +91,11 @@ public class StorageSyncJob extends BaseJob { return; } + if (!TextSecurePreferences.isPushRegistered(context)) { + Log.i(TAG, "Not registered. Skipping."); + return; + } + try { boolean needsMultiDeviceSync = performSync(); @@ -113,11 +111,6 @@ public class StorageSyncJob extends BaseJob { .then(new StorageForcePushJob()) .then(new MultiDeviceStorageSyncRequestJob()) .enqueue(); - } finally { - if (!SignalStore.storageServiceValues().hasFirstStorageSyncCompleted()) { - SignalStore.storageServiceValues().setFirstStorageSyncCompleted(true); - ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(false)); - } } } @@ -146,17 +139,20 @@ public class StorageSyncJob extends BaseJob { if (remoteManifest.isPresent() && remoteManifestVersion > localManifestVersion) { Log.i(TAG, "[Remote Newer] Newer manifest version found!"); - List allLocalStorageKeys = getAllLocalStorageKeys(context); - KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageIds(), allLocalStorageKeys); + List allLocalStorageKeys = getAllLocalStorageIds(context); + KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageIds(), allLocalStorageKeys); if (!keyDifference.isEmpty()) { Log.i(TAG, "[Remote Newer] There's a difference in keys. Local-only: " + keyDifference.getLocalOnlyKeys().size() + ", Remote-only: " + keyDifference.getRemoteOnlyKeys().size()); - List localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys()); + Set archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients(); + List localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys(), archivedRecipients); List remoteOnly = accountManager.readStorageRecords(storageServiceKey, keyDifference.getRemoteOnlyKeys()); MergeResult mergeResult = StorageSyncHelper.resolveConflict(remoteOnly, localOnly); WriteOperationResult writeOperationResult = StorageSyncHelper.createWriteOperation(remoteManifest.get().getVersion(), allLocalStorageKeys, mergeResult); + StorageSyncValidations.validate(writeOperationResult); + Log.i(TAG, "[Remote Newer] MergeResult :: " + mergeResult); if (!writeOperationResult.isEmpty()) { @@ -182,6 +178,7 @@ public class StorageSyncJob extends BaseJob { recipientDatabase.applyStorageSyncUpdates(mergeResult.getLocalContactInserts(), mergeResult.getLocalContactUpdates(), mergeResult.getLocalGroupV1Inserts(), mergeResult.getLocalGroupV1Updates()); storageKeyDatabase.applyStorageSyncUpdates(mergeResult.getLocalUnknownInserts(), mergeResult.getLocalUnknownDeletes()); + StorageSyncHelper.applyAccountStorageSyncUpdates(context, mergeResult.getLocalAccountUpdate()); needsMultiDeviceSync = true; Log.i(TAG, "[Remote Newer] Updating local manifest version to: " + remoteManifestVersion); @@ -195,20 +192,27 @@ public class StorageSyncJob extends BaseJob { localManifestVersion = TextSecurePreferences.getStorageManifestVersion(context); - List allLocalStorageKeys = recipientDatabase.getAllStorageSyncKeys(); - List pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates(); - List pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions(); - List pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions(); - Optional localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion, - allLocalStorageKeys, - pendingUpdates, - pendingInsertions, - pendingDeletions); + List allLocalStorageKeys = getAllLocalStorageIds(context); + List pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates(); + List pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions(); + List pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions(); + Optional pendingAccountUpdate = StorageSyncHelper.getPendingAccountSyncUpdate(context); + Optional pendingAccountInsert = StorageSyncHelper.getPendingAccountSyncInsert(context); + Set archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients(); + Optional localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion, + allLocalStorageKeys, + pendingUpdates, + pendingInsertions, + pendingDeletions, + pendingAccountUpdate, + pendingAccountInsert, + archivedRecipients); if (localWriteResult.isPresent()) { - Log.i(TAG, String.format(Locale.ENGLISH, "[Local Changes] Local changes present. %d updates, %d inserts, %d deletes.", pendingUpdates.size(), pendingInsertions.size(), pendingDeletions.size())); + 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); @@ -216,18 +220,19 @@ public class StorageSyncJob extends BaseJob { throw new AssertionError("Decided there were local writes, but our write result was empty!"); } - Optional conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes()); + Optional 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 clearIds = new ArrayList<>(pendingUpdates.size() + pendingInsertions.size() + pendingDeletions.size()); + List 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()); @@ -243,22 +248,44 @@ public class StorageSyncJob extends BaseJob { return needsMultiDeviceSync; } - private static @NonNull List getAllLocalStorageKeys(@NonNull Context context) { - return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getAllStorageSyncKeys(), + private static @NonNull List getAllLocalStorageIds(@NonNull Context context) { + Recipient self = Recipient.self().fresh(); + + return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getContactStorageSyncIds(), + Collections.singletonList(StorageId.forAccount(self.getStorageServiceId())), DatabaseFactory.getStorageKeyDatabase(context).getAllKeys()); } - private static @NonNull List buildLocalStorageRecords(@NonNull Context context, @NonNull List ids) { + private static @NonNull List buildLocalStorageRecords(@NonNull Context context, @NonNull List ids, @NonNull Set archivedRecipients) { RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context); StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context); List records = new ArrayList<>(ids.size()); for (StorageId id : ids) { - SignalStorageRecord record = Optional.fromNullable(recipientDatabase.getByStorageId(id.getRaw())) - .transform(StorageSyncModels::localToRemoteRecord) - .or(() -> storageKeyDatabase.getById(id.getRaw())); - records.add(record); + 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) { + records.add(StorageSyncModels.localToRemoteRecord(settings, archivedRecipients)); + } else { + Log.w(TAG, "Missing local recipient model! Type: " + id.getType()); + } + break; + case ManifestRecord.Identifier.Type.ACCOUNT_VALUE: + records.add(StorageSyncHelper.buildAccountRecord(context, id)); + 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; 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 603f1dd7ae..1ec6382866 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.java @@ -18,7 +18,6 @@ public final class SignalStore { public static void onFirstEverAppLaunch() { registrationValues().onFirstEverAppLaunch(); - storageServiceValues().setFirstStorageSyncCompleted(false); } public static @NonNull KbsValues kbsValues() { 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 0d5cf65e07..4349bed772 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StorageServiceValues.java @@ -9,9 +9,8 @@ import java.security.SecureRandom; public class StorageServiceValues { - private static final String STORAGE_MASTER_KEY = "storage.storage_master_key"; - private static final String FIRST_STORAGE_SYNC_COMPLETED = "storage.first_storage_sync_completed"; - private static final String LAST_SYNC_TIME = "storage.last_sync_time"; + private static final String STORAGE_MASTER_KEY = "storage.storage_master_key"; + private static final String LAST_SYNC_TIME = "storage.last_sync_time"; private final KeyValueStore store; @@ -38,14 +37,6 @@ public class StorageServiceValues { .commit(); } - public boolean hasFirstStorageSyncCompleted() { - return !FeatureFlags.storageServiceRestore() || store.getBoolean(FIRST_STORAGE_SYNC_COMPLETED, true); - } - - public void setFirstStorageSyncCompleted(boolean completed) { - store.beginWrite().putBoolean(FIRST_STORAGE_SYNC_COMPLETED, completed).apply(); - } - public long getLastSyncTime() { return store.getLong(LAST_SYNC_TIME, 0); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java index a7cd846d2c..fc88ec5749 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/AppProtectionPreferenceFragment.java @@ -20,16 +20,23 @@ import org.thoughtcrime.securesms.PassphraseChangeActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.SwitchPreferenceCompat; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.database.Database; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob; import org.thoughtcrime.securesms.jobs.RefreshAttributesJob; +import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.lock.RegistrationLockDialog; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.lock.v2.PinUtil; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.KeyCachingService; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -221,12 +228,16 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment private class ReadReceiptToggleListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = (boolean)newValue; - ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(enabled, - TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()), - TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()), - TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); + SignalExecutors.BOUNDED.execute(() -> { + boolean enabled = (boolean)newValue; + DatabaseFactory.getRecipientDatabase(getContext()).markNeedsSync(Recipient.self().getId()); + StorageSyncHelper.scheduleSyncForDataChange(); + ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(enabled, + TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()), + TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()), + TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); + }); return true; } } @@ -234,16 +245,19 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment private class TypingIndicatorsToggleListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = (boolean)newValue; - ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()), - enabled, - TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()), - TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); - - if (!enabled) { - ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().clear(); - } + SignalExecutors.BOUNDED.execute(() -> { + boolean enabled = (boolean)newValue; + DatabaseFactory.getRecipientDatabase(getContext()).markNeedsSync(Recipient.self().getId()); + StorageSyncHelper.scheduleSyncForDataChange(); + ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()), + enabled, + TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()), + TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); + if (!enabled) { + ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().clear(); + } + }); return true; } } @@ -251,12 +265,15 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment private class LinkPreviewToggleListener implements Preference.OnPreferenceChangeListener { @Override public boolean onPreferenceChange(Preference preference, Object newValue) { - boolean enabled = (boolean)newValue; - ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()), - TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()), - TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()), - enabled)); - + SignalExecutors.BOUNDED.execute(() -> { + boolean enabled = (boolean)newValue; + DatabaseFactory.getRecipientDatabase(getContext()).markNeedsSync(Recipient.self().getId()); + StorageSyncHelper.scheduleSyncForDataChange(); + ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()), + TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()), + TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()), + enabled)); + }); return true; } } @@ -355,10 +372,14 @@ public class AppProtectionPreferenceFragment extends CorrectedPreferenceFragment @Override public boolean onPreferenceChange(Preference preference, Object newValue) { boolean enabled = (boolean) newValue; - ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(getContext()), - TextSecurePreferences.isTypingIndicatorsEnabled(getContext()), - enabled, - TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); + SignalExecutors.BOUNDED.execute(() -> { + DatabaseFactory.getRecipientDatabase(getContext()).markNeedsSync(Recipient.self().getId()); + StorageSyncHelper.scheduleSyncForDataChange(); + ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(getContext()), + TextSecurePreferences.isTypingIndicatorsEnabled(getContext()), + enabled, + TextSecurePreferences.isLinkPreviewsEnabled(getContext()))); + }); return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 0e652bd523..d3db7c6f91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -93,7 +93,7 @@ public class Recipient { private final Capability uuidCapability; private final Capability groupsV2Capability; private final InsightsBannerTier insightsBannerTier; - private final byte[] storageKey; + private final byte[] storageId; private final byte[] identityKey; private final VerifiedStatus identityStatus; @@ -326,7 +326,7 @@ public class Recipient { this.forceSmsSelection = false; this.uuidCapability = Capability.UNKNOWN; this.groupsV2Capability = Capability.UNKNOWN; - this.storageKey = null; + this.storageId = null; this.identityKey = null; this.identityStatus = VerifiedStatus.DEFAULT; } @@ -367,7 +367,7 @@ public class Recipient { this.forceSmsSelection = details.forceSmsSelection; this.uuidCapability = details.uuidCapability; this.groupsV2Capability = details.groupsV2Capability; - this.storageKey = details.storageKey; + this.storageId = details.storageId; this.identityKey = details.identityKey; this.identityStatus = details.identityStatus; } @@ -706,8 +706,8 @@ public class Recipient { return profileKeyCredential != null; } - public @Nullable byte[] getStorageServiceKey() { - return storageKey; + public @Nullable byte[] getStorageServiceId() { + return storageId; } public @NonNull VerifiedStatus getIdentityVerifiedStatus() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index b4394dde2f..5a659a2692 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -58,7 +58,7 @@ public class RecipientDetails { final Recipient.Capability uuidCapability; final Recipient.Capability groupsV2Capability; final InsightsBannerTier insightsBannerTier; - final byte[] storageKey; + final byte[] storageId; final byte[] identityKey; final VerifiedStatus identityStatus; @@ -103,7 +103,7 @@ public class RecipientDetails { this.uuidCapability = settings.getUuidCapability(); this.groupsV2Capability = settings.getGroupsV2Capability(); this.insightsBannerTier = settings.getInsightsBannerTier(); - this.storageKey = settings.getStorageKey(); + this.storageId = settings.getStorageId(); this.identityKey = settings.getIdentityKey(); this.identityStatus = settings.getIdentityStatus(); @@ -149,7 +149,7 @@ public class RecipientDetails { this.name = null; this.uuidCapability = Recipient.Capability.UNKNOWN; this.groupsV2Capability = Recipient.Capability.UNKNOWN; - this.storageKey = null; + this.storageId = null; this.identityKey = null; this.identityStatus = VerifiedStatus.DEFAULT; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java index 18833b73de..e9bd30fa86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java @@ -20,8 +20,8 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceMessageRequestResponseJob; import org.thoughtcrime.securesms.jobs.RotateProfileKeyJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.util.GroupUtil; @@ -87,7 +87,7 @@ public class RecipientUtil { } ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob()); - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); } @WorkerThread @@ -98,7 +98,7 @@ public class RecipientUtil { DatabaseFactory.getRecipientDatabase(context).setBlocked(recipient.getId(), false); ApplicationDependencies.getJobManager().add(new MultiDeviceBlockedUpdateJob()); - ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + StorageSyncHelper.scheduleSyncForDataChange(); if (FeatureFlags.messageRequests()) { ApplicationDependencies.getJobManager().add(MultiDeviceMessageRequestResponseJob.forAccept(recipient.getId())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java index 7089f5e00e..90c04f68d7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RegistrationLockFragment.java @@ -21,7 +21,7 @@ import com.dd.CircularProgressButton; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; -import org.thoughtcrime.securesms.jobs.StorageSyncJob; +import org.thoughtcrime.securesms.jobs.StorageAccountRestoreJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.PinKeyboardType; import org.thoughtcrime.securesms.logging.Log; @@ -308,14 +308,14 @@ public final class RegistrationLockFragment extends BaseRegistrationFragment { if (FeatureFlags.storageServiceRestore()) { long startTime = System.currentTimeMillis(); SimpleTask.run(() -> { - return ApplicationDependencies.getJobManager().runSynchronously(new StorageSyncJob(), TimeUnit.SECONDS.toMillis(10)); + return ApplicationDependencies.getJobManager().runSynchronously(new StorageAccountRestoreJob(), StorageAccountRestoreJob.LIFESPAN); }, result -> { long elapsedTime = System.currentTimeMillis() - startTime; if (result.isPresent()) { - Log.i(TAG, "Storage Service restore completed: " + result.get().name() + ". (Took " + elapsedTime + " ms)"); + Log.i(TAG, "Storage Service account restore completed: " + result.get().name() + ". (Took " + elapsedTime + " ms)"); } else { - Log.i(TAG, "Storage Service restore failed to complete in the allotted time. (" + elapsedTime + " ms elapsed)"); + Log.i(TAG, "Storage Service account restore failed to complete in the allotted time. (" + elapsedTime + " ms elapsed)"); } cancelSpinning(pinButton); Navigation.findNavController(requireView()).navigate(RegistrationLockFragmentDirections.actionSuccessfulRegistration()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java new file mode 100644 index 0000000000..62dbf3613d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java @@ -0,0 +1,116 @@ +package org.thoughtcrime.securesms.storage; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.logging.Log; +import org.whispersystems.libsignal.util.guava.Optional; +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.internal.storage.protos.ContactRecord.IdentityState; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +class AccountConflictMerger implements StorageSyncHelper.ConflictMerger { + + private static final String TAG = Log.tag(AccountConflictMerger.class); + + private final Optional local; + + AccountConflictMerger(Optional local) { + this.local = local; + } + + @Override + public @NonNull Optional getMatching(@NonNull SignalAccountRecord record) { + return local; + } + + @Override + public @NonNull Collection getInvalidEntries(@NonNull Collection remoteRecords) { + Set invalid = new HashSet<>(remoteRecords); + if (remoteRecords.size() > 0) { + invalid.remove(remoteRecords.iterator().next()); + } + + if (invalid.size() > 0) { + Log.w(TAG, "Found invalid account entries! Count: " + invalid.size()); + } + + return invalid; + } + + @Override + public @NonNull SignalAccountRecord merge(@NonNull SignalAccountRecord remote, @NonNull SignalAccountRecord local, @NonNull StorageSyncHelper.KeyGenerator 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(""); + } + + String avatarUrlPath = remote.getAvatarUrlPath().or(local.getAvatarUrlPath()).or(""); + byte[] profileKey = remote.getProfileKey().or(local.getProfileKey()).orNull(); + boolean noteToSelfArchived = remote.isNoteToSelfArchived(); + boolean readReceipts = remote.isReadReceiptsEnabled(); + boolean typingIndicators = remote.isTypingIndicatorsEnabled(); + boolean sealedSenderIndicators = remote.isSealedSenderIndicatorsEnabled(); + boolean linkPreviews = remote.isLinkPreviewsEnabled(); + boolean matchesRemote = doParamsMatch(remote, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews); + boolean matchesLocal = doParamsMatch(local, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews); + + if (matchesRemote) { + return remote; + } else if (matchesLocal) { + return local; + } else { + return new SignalAccountRecord.Builder(keyGenerator.generate()) + .setGivenName(givenName) + .setFamilyName(familyName) + .setAvatarUrlPath(avatarUrlPath) + .setProfileKey(profileKey) + .setNoteToSelfArchived(noteToSelfArchived) + .setReadReceiptsEnabled(readReceipts) + .setTypingIndicatorsEnabled(typingIndicators) + .setSealedSenderIndicatorsEnabled(sealedSenderIndicators) + .setLinkPreviewsEnabled(linkPreviews) + .build(); + } + } + + private static boolean doParamsMatch(@NonNull SignalAccountRecord contact, + @NonNull String givenName, + @NonNull String familyName, + @NonNull String avatarUrlPath, + @Nullable byte[] profileKey, + boolean noteToSelfArchived, + boolean readReceipts, + boolean typingIndicators, + boolean sealedSenderIndicators, + boolean linkPreviewsEnabled) + { + return 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.isReadReceiptsEnabled() == readReceipts && + contact.isTypingIndicatorsEnabled() == typingIndicators && + contact.isSealedSenderIndicatorsEnabled() == sealedSenderIndicators && + contact.isLinkPreviewsEnabled() == linkPreviewsEnabled; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java index 46c835c6df..b121449b48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/ContactConflictMerger.java @@ -3,6 +3,10 @@ package org.thoughtcrime.securesms.storage; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.SignalContactRecord; @@ -11,16 +15,21 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id import java.util.Arrays; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; class ContactConflictMerger implements StorageSyncHelper.ConflictMerger { + private static final String TAG = Log.tag(ContactConflictMerger.class); + private final Map localByUuid = new HashMap<>(); private final Map localByE164 = new HashMap<>(); - ContactConflictMerger(@NonNull Collection localOnly) { + private final Recipient self; + + ContactConflictMerger(@NonNull Collection localOnly, @NonNull Recipient self) { for (SignalContactRecord contact : localOnly) { if (contact.getAddress().getUuid().isPresent()) { localByUuid.put(contact.getAddress().getUuid().get(), contact); @@ -29,6 +38,8 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger getInvalidEntries(@NonNull Collection remoteRecords) { + List invalid = Stream.of(remoteRecords) + .filter(r -> r.getAddress().getUuid().equals(self.getUuid()) || r.getAddress().getNumber().equals(self.getE164())) + .toList(); + if (invalid.size() > 0) { + Log.w(TAG, "Found invalid contact entries! Count: " + invalid.size()); + } + + return invalid; + } + @Override public @NonNull SignalContactRecord merge(@NonNull SignalContactRecord remote, @NonNull SignalContactRecord local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) { String givenName; diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMerger.java index 05f54e7a77..079a48a67e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMerger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV1ConflictMerger.java @@ -10,6 +10,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; import java.util.Collection; +import java.util.Collections; import java.util.Map; class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger { @@ -25,6 +26,11 @@ class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger getInvalidEntries(@NonNull Collection remoteRecords) { + return Collections.emptySet(); + } + @Override public @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) { boolean blocked = remote.isBlocked(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMerger.java index 85c86c547e..a8f37dae15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMerger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/GroupV2ConflictMerger.java @@ -10,6 +10,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; import java.util.Collection; +import java.util.Collections; import java.util.Map; class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger { @@ -25,6 +26,11 @@ class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger getInvalidEntries(@NonNull Collection remoteRecords) { + return Collections.emptySet(); + } + @Override public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) { boolean blocked = remote.isBlocked(); 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 dc7d1e5b93..31c444d089 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.java @@ -1,17 +1,29 @@ package org.thoughtcrime.securesms.storage; +import android.content.Context; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.annimon.stream.Stream; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; +import org.thoughtcrime.securesms.jobs.StorageSyncJob; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.profiles.ProfileName; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.SetUtil; +import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; import org.whispersystems.signalservice.api.storage.SignalRecord; @@ -31,6 +43,9 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.crypto.KeyGenerator; public final class StorageSyncHelper { @@ -40,6 +55,8 @@ public final class StorageSyncHelper { private static KeyGenerator keyGenerator = KEY_GENERATOR; + 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 @@ -56,11 +73,15 @@ public final class StorageSyncHelper { * @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()}. */ + // TODO [greyson] [storage] Test this public static @NonNull Optional buildStorageUpdatesForLocal(long currentManifestVersion, @NonNull List currentLocalKeys, @NonNull List updates, @NonNull List inserts, - @NonNull List deletes) + @NonNull List deletes, + @NonNull Optional accountUpdate, + @NonNull Optional accountInsert, + @NonNull Set archivedRecipients) { Set completeKeys = new LinkedHashSet<>(currentLocalKeys); Set storageInserts = new LinkedHashSet<>(); @@ -68,33 +89,48 @@ public final class StorageSyncHelper { Map storageKeyUpdates = new HashMap<>(); for (RecipientSettings insert : inserts) { - storageInserts.add(StorageSyncModels.localToRemoteRecord(insert)); + storageInserts.add(StorageSyncModels.localToRemoteRecord(insert, archivedRecipients)); + } + + if (accountInsert.isPresent()) { + storageInserts.add(SignalStorageRecord.forAccount(accountInsert.get())); } for (RecipientSettings delete : deletes) { - byte[] key = Objects.requireNonNull(delete.getStorageKey()); + byte[] key = Objects.requireNonNull(delete.getStorageId()); storageDeletes.add(ByteBuffer.wrap(key)); completeKeys.remove(StorageId.forContact(key)); } for (RecipientSettings update : updates) { - byte[] oldKey = Objects.requireNonNull(update.getStorageKey()); + byte[] oldKey = update.getStorageId(); byte[] newKey = generateKey(); - storageInserts.add(StorageSyncModels.localToRemoteRecord(update, newKey)); + storageInserts.add(StorageSyncModels.localToRemoteRecord(update, newKey, archivedRecipients)); storageDeletes.add(ByteBuffer.wrap(oldKey)); completeKeys.remove(StorageId.forContact(oldKey)); completeKeys.add(StorageId.forContact(newKey)); storageKeyUpdates.put(update.getId(), newKey); } + if (accountUpdate.isPresent()) { + byte[] oldKey = accountUpdate.get().getId().getRaw(); + byte[] newKey = generateKey(); + + storageInserts.add(SignalStorageRecord.forAccount(StorageId.forAccount(newKey), accountUpdate.get())); + storageDeletes.add(ByteBuffer.wrap(oldKey)); + completeKeys.remove(StorageId.forAccount(oldKey)); + completeKeys.add(StorageId.forAccount(newKey)); + storageKeyUpdates.put(Recipient.self().getId(), newKey); + } + if (storageInserts.isEmpty() && storageDeletes.isEmpty()) { return Optional.absent(); } else { - List contactDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList(); - List completeKeysBytes = new ArrayList<>(completeKeys); - SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, completeKeysBytes); - WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), contactDeleteBytes); + List contactDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList(); + List completeKeysBytes = new ArrayList<>(completeKeys); + SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, completeKeysBytes); + WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), contactDeleteBytes); return Optional.of(new LocalWriteResult(writeOperationResult, storageKeyUpdates)); } @@ -110,8 +146,8 @@ public final class StorageSyncHelper { * @return An object describing which keys are exclusive to the remote data set and which keys are * exclusive to the local data set. */ - public static @NonNull KeyDifferenceResult findKeyDifference(@NonNull List remoteKeys, - @NonNull List localKeys) + public static @NonNull KeyDifferenceResult findKeyDifference(@NonNull Collection remoteKeys, + @NonNull Collection localKeys) { Set remoteOnlyKeys = SetUtil.difference(remoteKeys, localKeys); Set localOnlyKeys = SetUtil.difference(localKeys, remoteKeys); @@ -143,12 +179,23 @@ public final class StorageSyncHelper { List remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList(); List localOnlyUnknowns = Stream.of(localOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList(); - RecordMergeResult> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts)); - RecordMergeResult> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1)); + List remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList(); + List localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList(); + if (remoteOnlyAccount.size() > 0 && localOnlyAccount.isEmpty()) { + throw new AssertionError("Found a remote-only account, but no local-only account!"); + } + if (localOnlyAccount.size() > 1) { + throw new AssertionError("Multiple local accounts?"); + } + + RecordMergeResult contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self())); + RecordMergeResult groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1)); + RecordMergeResult accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0)))); Set remoteInserts = new HashSet<>(); remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList()); remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList()); + remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList()); Set> remoteUpdates = new HashSet<>(); remoteUpdates.addAll(Stream.of(contactMergeResult.remoteUpdates) @@ -157,6 +204,14 @@ public final class StorageSyncHelper { remoteUpdates.addAll(Stream.of(groupV1MergeResult.remoteUpdates) .map(c -> new RecordUpdate<>(SignalStorageRecord.forGroupV1(c.getOld()), SignalStorageRecord.forGroupV1(c.getNew()))) .toList()); + remoteUpdates.addAll(Stream.of(accountMergeResult.remoteUpdates) + .map(c -> new RecordUpdate<>(SignalStorageRecord.forAccount(c.getOld()), SignalStorageRecord.forAccount(c.getNew()))) + .toList()); + + Set remoteDeletes = new HashSet<>(); + remoteDeletes.addAll(contactMergeResult.remoteDeletes); + remoteDeletes.addAll(groupV1MergeResult.remoteDeletes); + remoteDeletes.addAll(accountMergeResult.remoteDeletes); return new MergeResult(contactMergeResult.localInserts, contactMergeResult.localUpdates, @@ -164,8 +219,10 @@ public final class StorageSyncHelper { groupV1MergeResult.localUpdates, new LinkedHashSet<>(remoteOnlyUnknowns), new LinkedHashSet<>(localOnlyUnknowns), + accountMergeResult.localUpdates.isEmpty() ? Optional.absent() : Optional.of(accountMergeResult.localUpdates.iterator().next()), remoteInserts, - remoteUpdates); + remoteUpdates, + remoteDeletes); } /** @@ -188,6 +245,7 @@ public final class StorageSyncHelper { inserts.addAll(Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getNew).toList()); List deletes = Stream.of(mergeResult.getRemoteUpdates()).map(RecordUpdate::getOld).map(SignalStorageRecord::getId).map(StorageId::getRaw).toList(); + deletes.addAll(Stream.of(mergeResult.getRemoteDeletes()).map(SignalRecord::getId).map(StorageId::getRaw).toList()); return new WriteOperationResult(manifest, inserts, deletes); } @@ -201,14 +259,18 @@ public final class StorageSyncHelper { keyGenerator = testKeyGenerator; } - private static @NonNull RecordMergeResult> resolveRecordConflict(@NonNull Collection remoteOnlyRecords, - @NonNull Collection localOnlyRecords, - @NonNull ConflictMerger merger) + private static @NonNull RecordMergeResult resolveRecordConflict(@NonNull Collection remoteOnlyRecords, + @NonNull Collection localOnlyRecords, + @NonNull ConflictMerger merger) { - Set localInserts = new LinkedHashSet<>(remoteOnlyRecords); - Set remoteInserts = new LinkedHashSet<>(localOnlyRecords); - Set> localUpdates = new LinkedHashSet<>(); - Set> remoteUpdates = new LinkedHashSet<>(); + Set localInserts = new HashSet<>(remoteOnlyRecords); + Set remoteInserts = new HashSet<>(localOnlyRecords); + Set> localUpdates = new HashSet<>(); + Set> remoteUpdates = new HashSet<>(); + Set remoteDeletes = new HashSet<>(merger.getInvalidEntries(remoteOnlyRecords)); + + remoteOnlyRecords.removeAll(remoteDeletes); + localInserts.removeAll(remoteDeletes); for (E remote : remoteOnlyRecords) { Optional local = merger.getMatching(remote); @@ -229,13 +291,84 @@ public final class StorageSyncHelper { } } - return new RecordMergeResult<>(localInserts, localUpdates, remoteInserts, remoteUpdates); + return new RecordMergeResult<>(localInserts, localUpdates, remoteInserts, remoteUpdates, remoteDeletes); } public static boolean profileKeyChanged(RecordUpdate update) { return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey()); } + public static Optional getPendingAccountSyncUpdate(@NonNull Context context) { + if (DatabaseFactory.getRecipientDatabase(context).getDirtyState(Recipient.self().getId()) != RecipientDatabase.DirtyState.UPDATE) { + return Optional.absent(); + } + return Optional.of(buildAccountRecord(context, null).getAccount().get()); + } + + public static Optional getPendingAccountSyncInsert(@NonNull Context context) { + if (DatabaseFactory.getRecipientDatabase(context).getDirtyState(Recipient.self().getId()) != RecipientDatabase.DirtyState.INSERT) { + return Optional.absent(); + } + return Optional.of(buildAccountRecord(context, null).getAccount().get()); + } + + public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @Nullable StorageId id) { + Recipient self = Recipient.self().fresh(); + SignalAccountRecord account = new SignalAccountRecord.Builder(id != null ? id.getRaw() : self.getStorageServiceId()) + .setProfileKey(self.getProfileKey()) + .setGivenName(self.getProfileName().getGivenName()) + .setFamilyName(self.getProfileName().getFamilyName()) + .setAvatarUrlPath(self.getProfileAvatar()) + .setNoteToSelfArchived(DatabaseFactory.getThreadDatabase(context).isArchived(self.getId())) + .setTypingIndicatorsEnabled(TextSecurePreferences.isTypingIndicatorsEnabled(context)) + .setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context)) + .setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)) + .setLinkPreviewsEnabled(TextSecurePreferences.isLinkPreviewsEnabled(context)) + .build(); + + return SignalStorageRecord.forAccount(account); + } + + public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional> update) { + if (!update.isPresent()) { + return; + } + applyAccountStorageSyncUpdates(context, update.get().getOld().getId(), update.get().getNew()); + } + + public static void applyAccountStorageSyncUpdates(@NonNull Context context, @NonNull StorageId storageId, @NonNull SignalAccountRecord update) { + DatabaseFactory.getRecipientDatabase(context).applyStorageSyncUpdates(storageId, update); + DatabaseFactory.getThreadDatabase(context).setArchived(Recipient.self().getId(), update.isNoteToSelfArchived()); + + TextSecurePreferences.setProfileName(context, ProfileName.fromParts(update.getGivenName().orNull(), update.getFamilyName().orNull())); + TextSecurePreferences.setReadReceiptsEnabled(context, update.isReadReceiptsEnabled()); + TextSecurePreferences.setTypingIndicatorsEnabled(context, update.isTypingIndicatorsEnabled()); + TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.isSealedSenderIndicatorsEnabled()); + TextSecurePreferences.setLinkPreviewsEnabled(context, update.isLinkPreviewsEnabled()); + if (update.getAvatarUrlPath().isPresent()) { + ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), update.getAvatarUrlPath().get())); + } + } + + public static void scheduleSyncForDataChange() { + if (!SignalStore.registrationValues().isRegistrationComplete()) { + Log.d(TAG, "Registration still ongoing. Ignore sync request."); + return; + } + ApplicationDependencies.getJobManager().add(new StorageSyncJob()); + } + + public static void scheduleRoutineSync() { + long timeSinceLastSync = System.currentTimeMillis() - SignalStore.storageServiceValues().getLastSyncTime(); + + if (timeSinceLastSync > REFRESH_INTERVAL) { + Log.d(TAG, "Scheduling a sync. Last sync was " + timeSinceLastSync + " ms ago."); + scheduleSyncForDataChange(); + } else { + Log.d(TAG, "No need for sync. Last sync was " + timeSinceLastSync + " ms ago."); + } + } + public static final class KeyDifferenceResult { private final List remoteOnlyKeys; private final List localOnlyKeys; @@ -261,24 +394,28 @@ public final class StorageSyncHelper { } public static final class MergeResult { - private final Set localContactInserts; - private final Set> localContactUpdates; - private final Set localGroupV1Inserts; - private final Set> localGroupV1Updates; - private final Set localUnknownInserts; - private final Set localUnknownDeletes; - private final Set remoteInserts; - private final Set> remoteUpdates; + private final Set localContactInserts; + private final Set> localContactUpdates; + private final Set localGroupV1Inserts; + private final Set> localGroupV1Updates; + private final Set localUnknownInserts; + private final Set localUnknownDeletes; + private final Optional> localAccountUpdate; + private final Set remoteInserts; + private final Set> remoteUpdates; + private final Set remoteDeletes; @VisibleForTesting - MergeResult(@NonNull Set localContactInserts, - @NonNull Set> localContactUpdates, - @NonNull Set localGroupV1Inserts, - @NonNull Set> localGroupV1Updates, - @NonNull Set localUnknownInserts, - @NonNull Set localUnknownDeletes, - @NonNull Set remoteInserts, - @NonNull Set> remoteUpdates) + MergeResult(@NonNull Set localContactInserts, + @NonNull Set> localContactUpdates, + @NonNull Set localGroupV1Inserts, + @NonNull Set> localGroupV1Updates, + @NonNull Set localUnknownInserts, + @NonNull Set localUnknownDeletes, + @NonNull Optional> localAccountUpdate, + @NonNull Set remoteInserts, + @NonNull Set> remoteUpdates, + @NonNull Set remoteDeletes) { this.localContactInserts = localContactInserts; this.localContactUpdates = localContactUpdates; @@ -286,8 +423,10 @@ public final class StorageSyncHelper { this.localGroupV1Updates = localGroupV1Updates; this.localUnknownInserts = localUnknownInserts; this.localUnknownDeletes = localUnknownDeletes; + this.localAccountUpdate = localAccountUpdate; this.remoteInserts = remoteInserts; this.remoteUpdates = remoteUpdates; + this.remoteDeletes = remoteDeletes; } public @NonNull Set getLocalContactInserts() { @@ -314,6 +453,10 @@ public final class StorageSyncHelper { return localUnknownDeletes; } + public @NonNull Optional> getLocalAccountUpdate() { + return localAccountUpdate; + } + public @NonNull Set getRemoteInserts() { return remoteInserts; } @@ -322,6 +465,10 @@ public final class StorageSyncHelper { return remoteUpdates; } + public @NonNull Set getRemoteDeletes() { + return remoteDeletes; + } + @NonNull Set getAllNewRecords() { Set records = new HashSet<>(); @@ -332,6 +479,7 @@ public final class StorageSyncHelper { records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getNew).toList()); records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getNew).toList()); records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getNew).toList()); + if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getNew()); return records; } @@ -343,6 +491,8 @@ public final class StorageSyncHelper { records.addAll(Stream.of(localContactUpdates).map(RecordUpdate::getOld).toList()); records.addAll(Stream.of(localGroupV1Updates).map(RecordUpdate::getOld).toList()); records.addAll(Stream.of(remoteUpdates).map(RecordUpdate::getOld).toList()); + records.addAll(remoteDeletes); + if (localAccountUpdate.isPresent()) records.add(localAccountUpdate.get().getOld()); return records; } @@ -350,8 +500,8 @@ public final class StorageSyncHelper { @Override public @NonNull String toString() { return String.format(Locale.ENGLISH, - "localContactInserts: %d, localContactUpdates: %d, localGroupInserts: %d, localGroupUpdates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, remoteInserts: %d, remoteUpdates: %d", - localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), remoteInserts.size(), remoteUpdates.size()); + "localContactInserts: %d, localContactUpdates: %d, localGroupV1Inserts: %d, localGroupV1Updates: %d, localUnknownInserts: %d, localUnknownDeletes: %d, localAccountUpdate: %b, remoteInserts: %d, remoteUpdates: %d", + localContactInserts.size(), localContactUpdates.size(), localGroupV1Inserts.size(), localGroupV1Updates.size(), localUnknownInserts.size(), localUnknownDeletes.size(), localAccountUpdate.isPresent(), remoteInserts.size(), remoteUpdates.size()); } } @@ -446,26 +596,30 @@ public final class StorageSyncHelper { } } - private static class RecordMergeResult { - final Set localInserts; - final Set localUpdates; - final Set remoteInserts; - final Set remoteUpdates; + private static class RecordMergeResult { + final Set localInserts; + final Set> localUpdates; + final Set remoteInserts; + final Set> remoteUpdates; + final Set remoteDeletes; - RecordMergeResult(@NonNull Set localInserts, - @NonNull Set localUpdates, - @NonNull Set remoteInserts, - @NonNull Set remoteUpdates) + RecordMergeResult(@NonNull Set localInserts, + @NonNull Set> localUpdates, + @NonNull Set remoteInserts, + @NonNull Set> remoteUpdates, + @NonNull Set remoteDeletes) { this.localInserts = localInserts; this.localUpdates = localUpdates; this.remoteInserts = remoteInserts; this.remoteUpdates = remoteUpdates; + this.remoteDeletes = remoteDeletes; } } interface ConflictMerger { @NonNull Optional getMatching(@NonNull E record); + @NonNull Collection getInvalidEntries(@NonNull Collection remoteRecords); @NonNull E merge(@NonNull E remote, @NonNull E local, @NonNull KeyGenerator keyGenerator); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java index 711ddc43ad..f53797bd41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; +import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.GroupUtil; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.storage.SignalContactRecord; @@ -11,27 +12,29 @@ import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; +import java.util.Set; + public final class StorageSyncModels { private StorageSyncModels() {} - public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings) { - if (settings.getStorageKey() == null) { + public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull Set archived) { + if (settings.getStorageId() == null) { throw new AssertionError("Must have a storage key!"); } - return localToRemoteRecord(settings, settings.getStorageKey()); + return localToRemoteRecord(settings, settings.getStorageId(), archived); } - public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull byte[] rawStorageId) { + public static @NonNull SignalStorageRecord localToRemoteRecord(@NonNull RecipientSettings settings, @NonNull byte[] rawStorageId, @NonNull Set archived) { switch (settings.getGroupType()) { - case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId)); - case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId)); + case NONE: return SignalStorageRecord.forContact(localToRemoteContact(settings, rawStorageId, archived)); + case SIGNAL_V1: return SignalStorageRecord.forGroupV1(localToRemoteGroupV1(settings, rawStorageId, archived)); default: throw new AssertionError("Unsupported type!"); } } - private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] rawStorageId) { + private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set archived) { if (recipient.getUuid() == null && recipient.getE164() == null) { throw new AssertionError("Must have either a UUID or a phone number!"); } @@ -44,10 +47,11 @@ public final class StorageSyncModels { .setProfileSharingEnabled(recipient.isProfileSharing()) .setIdentityKey(recipient.getIdentityKey()) .setIdentityState(localToRemoteIdentityState(recipient.getIdentityStatus())) + .setArchived(archived.contains(recipient.getId())) .build(); } - private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] rawStorageId) { + private static @NonNull SignalGroupV1Record localToRemoteGroupV1(@NonNull RecipientSettings recipient, byte[] rawStorageId, @NonNull Set archived) { if (recipient.getGroupId() == null) { throw new AssertionError("Must have a groupId!"); } @@ -55,6 +59,7 @@ public final class StorageSyncModels { return new SignalGroupV1Record.Builder(rawStorageId, GroupUtil.getDecodedIdOrThrow(recipient.getGroupId())) .setBlocked(recipient.isBlocked()) .setProfileSharingEnabled(recipient.isProfileSharing()) + .setArchived(archived.contains(recipient.getId())) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java new file mode 100644 index 0000000000..f1e5ac42bf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java @@ -0,0 +1,93 @@ +package org.thoughtcrime.securesms.storage; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Collectors; +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.util.Base64; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.api.storage.StorageId; +import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; + +import java.util.HashSet; +import java.util.Set; + +public final class StorageSyncValidations { + + private StorageSyncValidations() {} + + public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result) { + Set allSet = new HashSet<>(result.getManifest().getStorageIds()); + Set insertSet = new HashSet<>(Stream.of(result.getInserts()).map(SignalStorageRecord::getId).toList()); + + int accountCount = 0; + for (StorageId id : result.getManifest().getStorageIds()) { + accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE ? 1 : 0; + } + + if (result.getInserts().size() > insertSet.size()) { + throw new DuplicateInsertInWriteError(); + } + + if (accountCount > 1) { + throw new MultipleAccountError(); + } + + if (accountCount == 0) { + throw new MissingAccountError(); + } + + for (SignalStorageRecord insert : result.getInserts()) { + if (!allSet.contains(insert.getId())) { + throw new InsertNotPresentInFullIdSetError(); + } + + if (insert.isUnknown()) { + throw new UnknownInsertError(); + } + + if (insert.getContact().isPresent()) { + Recipient self = Recipient.self().fresh(); + SignalServiceAddress address = insert.getContact().get().getAddress(); + if (self.getE164().get().equals(address.getNumber().or("")) || self.getUuid().get().equals(address.getUuid().orNull())) { + throw new SelfAddedAsContactError(); + } + } + } + + if (result.getDeletes().size() > 0) { + Set allSetEncoded = Stream.of(result.getManifest().getStorageIds()).map(StorageId::getRaw).map(Base64::encodeBytes).collect(Collectors.toSet()); + + for (byte[] delete : result.getDeletes()) { + String encoded = Base64.encodeBytes(delete); + if (allSetEncoded.contains(encoded)) { + throw new DeletePresentInFullIdSetError(); + } + } + } + } + + private static final class DuplicateInsertInWriteError extends Error { + } + + private static final class InsertNotPresentInFullIdSetError extends Error { + } + + private static final class DeletePresentInFullIdSetError extends Error { + } + + private static final class UnknownInsertError extends Error { + } + + private static final class MultipleAccountError extends Error { + } + + private static final class MissingAccountError extends Error { + } + + private static final class SelfAddedAsContactError extends Error { + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 06c3fd39af..01ed441726 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -39,6 +39,7 @@ import android.text.SpannableString; import android.text.TextUtils; import android.text.style.StyleSpan; +import com.annimon.stream.Stream; import com.google.android.mms.pdu_alt.CharacterSets; import com.google.android.mms.pdu_alt.EncodedStringValue; import com.google.i18n.phonenumbers.NumberParseException; @@ -595,11 +596,13 @@ public class Util { return handler; } - public static List concatenatedList(List first, List second) { - final List concat = new ArrayList<>(first.size() + second.size()); + @SafeVarargs + public static List concatenatedList(Collection ... items) { + final List concat = new ArrayList<>(Stream.of(items).reduce(0, (sum, list) -> sum + list.size())); - concat.addAll(first); - concat.addAll(second); + for (Collection list : items) { + concat.addAll(list); + } return concat; } diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java index fc889fdb5e..6233010d5f 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/ContactConflictMergerTest.java @@ -1,13 +1,16 @@ package org.thoughtcrime.securesms.storage; import org.junit.Test; -import org.thoughtcrime.securesms.storage.ContactConflictMerger; +import org.thoughtcrime.securesms.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; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; +import java.util.Collection; import java.util.Collections; import java.util.UUID; @@ -15,16 +18,30 @@ import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mock; +import static org.thoughtcrime.securesms.testutil.TestHelpers.assertContentsEqual; import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray; +import static org.thoughtcrime.securesms.testutil.TestHelpers.setOf; public class ContactConflictMergerTest { - private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7"); - private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1"); + private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7"); + private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1"); + private static final UUID UUID_SELF = UuidUtil.parseOrThrow("1b2a2ca5-fc9e-4656-8c9f-22cc349ed3af"); - private static final String E164_A = "+16108675309"; - private static final String E164_B = "+16101234567"; + + private static final String E164_A = "+16108675309"; + private static final String E164_B = "+16101234567"; + private static final String E164_SELF = "+16105555555"; + + private static final Recipient SELF = mock(Recipient.class); + static { + when(SELF.getUuid()).thenReturn(Optional.of(UUID_SELF)); + when(SELF.getE164()).thenReturn(Optional.of(E164_SELF)); + when(SELF.resolve()).thenReturn(SELF); + Log.initialize(new Log.Logger[0]); + } @Test public void merge_alwaysPreferRemote_exceptProfileSharingIsEitherOr() { @@ -51,7 +68,7 @@ public class ContactConflictMergerTest { .setArchived(true) .build(); - SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class)); + SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class)); assertEquals(UUID_A, merged.getAddress().getUuid().get()); assertEquals(E164_A, merged.getAddress().getNumber().get()); @@ -83,7 +100,7 @@ public class ContactConflictMergerTest { .setUsername("username B") .setProfileSharingEnabled(false) .build(); - SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class)); + SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class)); assertEquals(UUID_A, merged.getAddress().getUuid().get()); assertEquals(E164_B, merged.getAddress().getNumber().get()); @@ -113,7 +130,7 @@ public class ContactConflictMergerTest { .setFamilyName("BLast") .setProfileSharingEnabled(false) .build(); - SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class)); + SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class)); assertEquals(remote, merged); } @@ -125,8 +142,19 @@ public class ContactConflictMergerTest { .setGivenName("AFirst") .setFamilyName("ALast") .build(); - SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local)).merge(remote, local, mock(KeyGenerator.class)); + SignalContactRecord merged = new ContactConflictMerger(Collections.singletonList(local), SELF).merge(remote, local, mock(KeyGenerator.class)); assertEquals(local, merged); } + + @Test + public void getInvalidEntries_selfIsInvalid() { + SignalContactRecord a = new SignalContactRecord.Builder(byteArray(1), new SignalServiceAddress(UUID_A, E164_A)).build(); + SignalContactRecord b = new SignalContactRecord.Builder(byteArray(2), new SignalServiceAddress(UUID_B, E164_B)).build(); + SignalContactRecord self = new SignalContactRecord.Builder(byteArray(3), new SignalServiceAddress(UUID_SELF, E164_SELF)).build(); + + Collection invalid = new ContactConflictMerger(Collections.emptyList(), SELF).getInvalidEntries(setOf(a, b, self)); + + assertContentsEqual(setOf(self), invalid); + } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java index 7ce1ed31c3..3971f13b59 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java +++ b/app/src/test/java/org/thoughtcrime/securesms/storage/StorageSyncHelperTest.java @@ -2,15 +2,23 @@ package org.thoughtcrime.securesms.storage; import androidx.annotation.NonNull; +import com.airbnb.lottie.L; import com.annimon.stream.Stream; import org.junit.Before; import org.junit.Test; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; +import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.storage.StorageSyncHelper.KeyDifferenceResult; import org.thoughtcrime.securesms.storage.StorageSyncHelper.MergeResult; +import org.whispersystems.libsignal.util.guava.Optional; 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; @@ -29,31 +37,46 @@ import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.thoughtcrime.securesms.testutil.TestHelpers.assertByteListEquals; import static org.thoughtcrime.securesms.testutil.TestHelpers.assertContentsEqual; import static org.thoughtcrime.securesms.testutil.TestHelpers.byteArray; import static org.thoughtcrime.securesms.testutil.TestHelpers.byteListOf; import static org.thoughtcrime.securesms.testutil.TestHelpers.setOf; +@RunWith(PowerMockRunner.class) +@PrepareForTest({ Recipient.class }) public final class StorageSyncHelperTest { - private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7"); - private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1"); - private static final UUID UUID_C = UuidUtil.parseOrThrow("b5552203-2bca-44aa-b6f5-9f5d87a335b6"); - private static final UUID UUID_D = UuidUtil.parseOrThrow("94829a32-7199-4a7b-8fb4-7e978509ab84"); + private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7"); + private static final UUID UUID_B = UuidUtil.parseOrThrow("32119989-77fb-4e18-af70-81d55185c6b1"); + private static final UUID UUID_C = UuidUtil.parseOrThrow("b5552203-2bca-44aa-b6f5-9f5d87a335b6"); + private static final UUID UUID_D = UuidUtil.parseOrThrow("94829a32-7199-4a7b-8fb4-7e978509ab84"); + private static final UUID UUID_SELF = UuidUtil.parseOrThrow("1b2a2ca5-fc9e-4656-8c9f-22cc349ed3af"); - private static final String E164_A = "+16108675309"; - private static final String E164_B = "+16101234567"; - private static final String E164_C = "+16101112222"; - private static final String E164_D = "+16103334444"; + private static final String E164_A = "+16108675309"; + private static final String E164_B = "+16101234567"; + private static final String E164_C = "+16101112222"; + private static final String E164_D = "+16103334444"; + private static final String E164_SELF = "+16105555555"; private static final int UNKNOWN_TYPE = Integer.MAX_VALUE; - @Before - public void setup() { - StorageSyncHelper.setTestKeyGenerator(null); + private static final Recipient SELF = mock(Recipient.class); + static { + when(SELF.getUuid()).thenReturn(Optional.of(UUID_SELF)); + when(SELF.getE164()).thenReturn(Optional.of(E164_SELF)); + when(SELF.resolve()).thenReturn(SELF); } + @Before + public void setup() { + mockStatic(Recipient.class); + when(Recipient.self()).thenReturn(SELF); + Log.initialize(new Log.Logger[0]); + } @Test public void findKeyDifference_allOverlap() { @@ -87,6 +110,21 @@ public final class StorageSyncHelperTest { assertTrue(result.getLocalContactUpdates().isEmpty()); assertEquals(setOf(SignalStorageRecord.forContact(local1)), result.getRemoteInserts()); assertTrue(result.getRemoteUpdates().isEmpty()); + assertTrue(result.getRemoteDeletes().isEmpty()); + } + + @Test + public void resolveConflict_contact_deleteSelfContact() { + SignalContactRecord remote1 = contact(1, UUID_SELF, E164_SELF, "self"); + SignalContactRecord local1 = contact(2, UUID_A, E164_A, "a"); + + MergeResult result = StorageSyncHelper.resolveConflict(recordSetOf(remote1), recordSetOf(local1)); + + assertTrue(result.getLocalContactInserts().isEmpty()); + assertTrue(result.getLocalContactUpdates().isEmpty()); + assertEquals(setOf(record(local1)), result.getRemoteInserts()); + assertTrue(result.getRemoteUpdates().isEmpty()); + assertEquals(setOf(remote1), result.getRemoteDeletes()); } @Test @@ -99,9 +137,10 @@ public final class StorageSyncHelperTest { SignalContactRecord expectedMerge = contact(1, UUID_A, E164_A, "a"); assertTrue(result.getLocalContactInserts().isEmpty()); - assertEquals(setOf(contactUpdate(local1, expectedMerge)), result.getLocalContactUpdates()); + assertEquals(setOf(update(local1, expectedMerge)), result.getLocalContactUpdates()); assertTrue(result.getRemoteInserts().isEmpty()); assertTrue(result.getRemoteUpdates().isEmpty()); + assertTrue(result.getRemoteDeletes().isEmpty()); } @Test @@ -114,9 +153,10 @@ public final class StorageSyncHelperTest { SignalGroupV1Record expectedMerge = groupV1(1, 1, true, false); assertTrue(result.getLocalContactInserts().isEmpty()); - assertEquals(setOf(groupV1Update(local1, expectedMerge)), result.getLocalGroupV1Updates()); + assertEquals(setOf(update(local1, expectedMerge)), result.getLocalGroupV1Updates()); assertTrue(result.getRemoteInserts().isEmpty()); assertTrue(result.getRemoteUpdates().isEmpty()); + assertTrue(result.getRemoteDeletes().isEmpty()); } @Test @@ -132,6 +172,7 @@ public final class StorageSyncHelperTest { assertTrue(result.getLocalContactUpdates().isEmpty()); assertTrue(result.getRemoteInserts().isEmpty()); assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates()); + assertTrue(result.getRemoteDeletes().isEmpty()); } @Test @@ -147,10 +188,12 @@ public final class StorageSyncHelperTest { assertTrue(result.getLocalGroupV1Updates().isEmpty()); assertTrue(result.getRemoteInserts().isEmpty()); assertEquals(setOf(recordUpdate(remote1, expectedMerge)), result.getRemoteUpdates()); + assertTrue(result.getRemoteDeletes().isEmpty()); } @Test public void resolveConflict_unknowns() { + SignalStorageRecord account = SignalStorageRecord.forAccount(account(99)); SignalStorageRecord remote1 = unknown(3); SignalStorageRecord remote2 = unknown(4); SignalStorageRecord remote3 = SignalStorageRecord.forGroupV2(groupV2(100, 200, true, true)); @@ -158,12 +201,13 @@ public final class StorageSyncHelperTest { SignalStorageRecord local2 = unknown(2); SignalStorageRecord local3 = SignalStorageRecord.forGroupV2(groupV2(101, 201, true, true)); - MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, remote3), setOf(local1, local2, local3)); + MergeResult result = StorageSyncHelper.resolveConflict(setOf(remote1, remote2, remote3, account), setOf(local1, local2, local3, account)); assertTrue(result.getLocalContactInserts().isEmpty()); assertTrue(result.getLocalContactUpdates().isEmpty()); assertEquals(setOf(remote1, remote2, remote3), result.getLocalUnknownInserts()); assertEquals(setOf(local1, local2, local3), result.getLocalUnknownDeletes()); + assertTrue(result.getRemoteDeletes().isEmpty()); } @Test @@ -180,13 +224,16 @@ public final class StorageSyncHelperTest { SignalGroupV1Record remote4 = groupV1(7, 1, true, false); SignalGroupV1Record local4 = groupV1(8, 1, false, true); - SignalStorageRecord unknownRemote = unknown(9); - SignalStorageRecord unknownLocal = unknown(10); + SignalAccountRecord remote5 = account(9); + SignalAccountRecord local5 = account(10); + + SignalStorageRecord unknownRemote = unknown(11); + SignalStorageRecord unknownLocal = unknown(12); StorageSyncHelper.setTestKeyGenerator(new TestGenerator(111, 222)); - Set remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, unknownRemote); - Set localOnly = recordSetOf(local1, local2, local3, local4, unknownLocal); + Set remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, unknownRemote); + Set localOnly = recordSetOf(local1, local2, local3, local4, local5, unknownLocal); MergeResult result = StorageSyncHelper.resolveConflict(remoteOnly, localOnly); @@ -195,39 +242,43 @@ public final class StorageSyncHelperTest { SignalGroupV1Record merge4 = groupV1(222, 1, true, true); assertEquals(setOf(remote3), result.getLocalContactInserts()); - assertEquals(setOf(contactUpdate(local2, merge2)), result.getLocalContactUpdates()); - assertEquals(setOf(groupV1Update(local4, merge4)), result.getLocalGroupV1Updates()); + assertEquals(setOf(update(local2, merge2)), result.getLocalContactUpdates()); + assertEquals(setOf(update(local4, merge4)), result.getLocalGroupV1Updates()); assertEquals(setOf(SignalStorageRecord.forContact(local3)), result.getRemoteInserts()); assertEquals(setOf(recordUpdate(remote1, merge1), recordUpdate(remote2, merge2), recordUpdate(remote4, merge4)), result.getRemoteUpdates()); + assertEquals(Optional.of(update(local5, remote5)), result.getLocalAccountUpdate()); assertEquals(setOf(unknownRemote), result.getLocalUnknownInserts()); assertEquals(setOf(unknownLocal), result.getLocalUnknownDeletes()); + assertTrue(result.getRemoteDeletes().isEmpty()); } @Test public void createWriteOperation_generic() { - List localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100)); - SignalContactRecord insert1 = contact(6, UUID_A, E164_A, "a" ); - SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b" ); - SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z" ); - SignalContactRecord insert2 = contact(7, UUID_C, E164_C, "c" ); - SignalContactRecord old2 = contact(2, UUID_D, E164_D, "d" ); - SignalContactRecord new2 = contact(8, UUID_D, E164_D, "z2"); - SignalGroupV1Record insert3 = groupV1(9, 1, true, true ); - SignalGroupV1Record old3 = groupV1(100, 1, true, true ); - SignalGroupV1Record new3 = groupV1(10, 1, false, true); - SignalStorageRecord unknownInsert = unknown(11); - SignalStorageRecord unknownDelete = unknown(12); + List localKeys = Arrays.asList(contactKey(1), contactKey(2), contactKey(3), contactKey(4), groupV1Key(100)); + SignalContactRecord insert1 = contact(6, UUID_A, E164_A, "a"); + SignalContactRecord old1 = contact(1, UUID_B, E164_B, "b"); + SignalContactRecord new1 = contact(5, UUID_B, E164_B, "z"); + SignalContactRecord insert2 = contact(7, UUID_C, E164_C, "c"); + SignalContactRecord old2 = contact(2, UUID_D, E164_D, "d"); + SignalContactRecord new2 = contact(8, UUID_D, E164_D, "z2"); + SignalGroupV1Record insert3 = groupV1(9, 1, true, true); + SignalGroupV1Record old3 = groupV1(100, 1, true, true); + SignalGroupV1Record new3 = groupV1(10, 1, false, true); + SignalStorageRecord unknownInsert = unknown(11); + SignalStorageRecord unknownDelete = unknown(12); StorageSyncHelper.WriteOperationResult result = StorageSyncHelper.createWriteOperation(1, localKeys, new MergeResult(setOf(insert2), - setOf(contactUpdate(old2, new2)), + setOf(update(old2, new2)), setOf(insert3), - setOf(groupV1Update(old3, new3)), + setOf(update(old3, new3)), setOf(unknownInsert), setOf(unknownDelete), + Optional.absent(), recordSetOf(insert1, insert3), - setOf(recordUpdate(old1, new1), recordUpdate(old3, new3)))); + setOf(recordUpdate(old1, new1), recordUpdate(old3, new3)), + setOf())); assertEquals(2, result.getManifest().getVersion()); assertContentsEqual(Arrays.asList(contactKey(3), contactKey(4), contactKey(5), contactKey(6), contactKey(7), contactKey(8), groupV1Key(9), groupV1Key(10), unknownKey(11)), result.getManifest().getStorageIds()); @@ -247,7 +298,7 @@ public final class StorageSyncHelperTest { assertEquals(a, b); assertEquals(a.hashCode(), b.hashCode()); - assertFalse(StorageSyncHelper.profileKeyChanged(contactUpdate(a, b))); + assertFalse(StorageSyncHelper.profileKeyChanged(update(a, b))); } @Test @@ -262,27 +313,33 @@ public final class StorageSyncHelperTest { assertNotEquals(a, b); assertNotEquals(a.hashCode(), b.hashCode()); - assertTrue(StorageSyncHelper.profileKeyChanged(contactUpdate(a, b))); + assertTrue(StorageSyncHelper.profileKeyChanged(update(a, b))); } private static Set recordSetOf(SignalRecord... records) { LinkedHashSet storageRecords = new LinkedHashSet<>(); for (SignalRecord record : records) { - if (record instanceof SignalContactRecord) { - storageRecords.add(SignalStorageRecord.forContact(record.getId(), (SignalContactRecord) record)); - } else if (record instanceof SignalGroupV1Record) { - storageRecords.add(SignalStorageRecord.forGroupV1(record.getId(), (SignalGroupV1Record) record)); - } else if (record instanceof SignalGroupV2Record) { - storageRecords.add(SignalStorageRecord.forGroupV2(record.getId(), (SignalGroupV2Record) record)); - } else { - storageRecords.add(SignalStorageRecord.forUnknown(record.getId())); - } + storageRecords.add(record(record)); } return storageRecords; } + private static SignalStorageRecord record(SignalRecord record) { + if (record instanceof SignalContactRecord) { + return SignalStorageRecord.forContact(record.getId(), (SignalContactRecord) record); + } else if (record instanceof SignalGroupV1Record) { + return SignalStorageRecord.forGroupV1(record.getId(), (SignalGroupV1Record) record); + } else if (record instanceof SignalGroupV2Record) { + return SignalStorageRecord.forGroupV2(record.getId(), (SignalGroupV2Record) record); + } else if (record instanceof SignalAccountRecord) { + return SignalStorageRecord.forAccount(record.getId(), (SignalAccountRecord) record); + } else { + return SignalStorageRecord.forUnknown(record.getId()); + } + } + private static Set recordSetOf(SignalGroupV1Record... groupRecords) { LinkedHashSet storageRecords = new LinkedHashSet<>(); @@ -302,6 +359,10 @@ public final class StorageSyncHelperTest { .setGivenName(profileName); } + private static SignalAccountRecord account(int key) { + return new SignalAccountRecord.Builder(byteArray(key)).build(); + } + private static SignalContactRecord contact(int key, UUID uuid, String e164, @@ -330,20 +391,12 @@ public final class StorageSyncHelperTest { } } - private static StorageSyncHelper.RecordUpdate contactUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) { - return new StorageSyncHelper.RecordUpdate<>(oldContact, newContact); + private static StorageSyncHelper.RecordUpdate update(E oldRecord, E newRecord) { + return new StorageSyncHelper.RecordUpdate<>(oldRecord, newRecord); } - private static StorageSyncHelper.RecordUpdate groupV1Update(SignalGroupV1Record oldGroup, SignalGroupV1Record newGroup) { - return new StorageSyncHelper.RecordUpdate<>(oldGroup, newGroup); - } - - private static StorageSyncHelper.RecordUpdate recordUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) { - return new StorageSyncHelper.RecordUpdate<>(SignalStorageRecord.forContact(oldContact), SignalStorageRecord.forContact(newContact)); - } - - private static StorageSyncHelper.RecordUpdate recordUpdate(SignalGroupV1Record oldGroup, SignalGroupV1Record newGroup) { - return new StorageSyncHelper.RecordUpdate<>(SignalStorageRecord.forGroupV1(oldGroup), SignalStorageRecord.forGroupV1(newGroup)); + private static StorageSyncHelper.RecordUpdate recordUpdate(E oldContact, E newContact) { + return new StorageSyncHelper.RecordUpdate<>(record(oldContact), record(newContact)); } private static SignalStorageRecord unknown(int key) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index e294acbafa..f411a02961 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -403,6 +403,18 @@ public class SignalServiceAccountManager { } } + public Optional getStorageManifest(StorageKey storageKey) throws IOException { + try { + String authToken = this.pushServiceSocket.getStorageAuth(); + StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken); + + return Optional.of(SignalStorageModels.remoteToLocalStorageManifest(storageManifest, storageKey)); + } catch (InvalidKeyException | NotFoundException e) { + Log.w(TAG, "Error while fetching manifest.", e); + return Optional.absent(); + } + } + public long getStorageManifestVersion() throws IOException { try { String authToken = this.pushServiceSocket.getStorageAuth(); @@ -431,6 +443,10 @@ public class SignalServiceAccountManager { } public List readStorageRecords(StorageKey storageKey, List storageKeys) throws IOException, InvalidKeyException { + if (storageKeys.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); ReadOperation.Builder operation = ReadOperation.newBuilder(); Map typeMap = new HashMap<>(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java new file mode 100644 index 0000000000..0081de665c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java @@ -0,0 +1,148 @@ +package org.whispersystems.signalservice.api.storage; + +import com.google.protobuf.ByteString; + +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.util.OptionalUtil; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; + +import java.util.Objects; + +public final class SignalAccountRecord implements SignalRecord { + + private final StorageId id; + private final AccountRecord proto; + + private final Optional givenName; + private final Optional familyName; + private final Optional avatarUrlPath; + private final Optional profileKey; + + public SignalAccountRecord(StorageId id, AccountRecord proto) { + this.id = id; + this.proto = proto; + + this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName()); + this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName()); + this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey()); + this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.getAvatarUrlPath()); + } + + @Override + public StorageId getId() { + return id; + } + + public Optional getGivenName() { + return givenName; + } + + public Optional getFamilyName() { + return familyName; + } + + public Optional getProfileKey() { + return profileKey; + } + + public Optional getAvatarUrlPath() { + return avatarUrlPath; + } + + public boolean isNoteToSelfArchived() { + return proto.getNoteToSelfArchived(); + } + + public boolean isReadReceiptsEnabled() { + return proto.getReadReceipts(); + } + + public boolean isTypingIndicatorsEnabled() { + return proto.getTypingIndicators(); + } + + public boolean isSealedSenderIndicatorsEnabled() { + return proto.getSealedSenderIndicators(); + } + + public boolean isLinkPreviewsEnabled() { + return proto.getLinkPreviews(); + } + + AccountRecord toProto() { + return proto; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SignalAccountRecord that = (SignalAccountRecord) o; + return id.equals(that.id) && + proto.equals(that.proto); + } + + @Override + public int hashCode() { + return Objects.hash(id, proto); + } + + public static final class Builder { + private final StorageId id; + private final AccountRecord.Builder builder; + + public Builder(byte[] rawId) { + this.id = StorageId.forAccount(rawId); + this.builder = AccountRecord.newBuilder(); + } + + public Builder setGivenName(String givenName) { + builder.setGivenName(givenName == null ? "" : givenName); + return this; + } + + public Builder setFamilyName(String familyName) { + builder.setFamilyName(familyName == null ? "" : familyName); + return this; + } + + public Builder setProfileKey(byte[] profileKey) { + builder.setProfileKey(profileKey == null ? ByteString.EMPTY : ByteString.copyFrom(profileKey)); + return this; + } + + public Builder setAvatarUrlPath(String urlPath) { + builder.setAvatarUrlPath(urlPath == null ? "" : urlPath); + return this; + } + + public Builder setNoteToSelfArchived(boolean archived) { + builder.setNoteToSelfArchived(archived); + return this; + } + + public Builder setReadReceiptsEnabled(boolean enabled) { + builder.setReadReceipts(enabled); + return this; + } + + public Builder setTypingIndicatorsEnabled(boolean enabled) { + builder.setTypingIndicators(enabled); + return this; + } + + public Builder setSealedSenderIndicatorsEnabled(boolean enabled) { + builder.setSealedSenderIndicators(enabled); + return this; + } + + public Builder setLinkPreviewsEnabled(boolean enabled) { + builder.setLinkPreviews(enabled); + return this; + } + + public SignalAccountRecord build() { + return new SignalAccountRecord(id, builder.build()); + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java index 62d5bd56de..f670257170 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalContactRecord.java @@ -23,7 +23,7 @@ public final class SignalContactRecord implements SignalRecord { private final Optional username; private final Optional identityKey; - private SignalContactRecord(StorageId id, ContactRecord proto) { + public SignalContactRecord(StorageId id, ContactRecord proto) { this.id = id; this.proto = proto; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java index e0556848ce..028d68ce75 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV1Record.java @@ -12,7 +12,7 @@ public final class SignalGroupV1Record implements SignalRecord { private final GroupV1Record proto; private final byte[] groupId; - private SignalGroupV1Record(StorageId id, GroupV1Record proto) { + public SignalGroupV1Record(StorageId id, GroupV1Record proto) { this.id = id; this.proto = proto; this.groupId = proto.getId().toByteArray(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java index 2436e151d0..3ad577d84f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalGroupV2Record.java @@ -15,7 +15,7 @@ public final class SignalGroupV2Record implements SignalRecord { private final GroupV2Record proto; private final GroupMasterKey masterKey; - private SignalGroupV2Record(StorageId id, GroupV2Record proto) { + public SignalGroupV2Record(StorageId id, GroupV2Record proto) { this.id = id; this.proto = proto; try { 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 ee18a7b0e7..d385242874 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,14 +1,33 @@ package org.whispersystems.signalservice.api.storage; +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.HashMap; import java.util.List; +import java.util.Map; public class SignalStorageManifest { - private final long version; - private final List storageIds; + private final long version; + private final List storageIds; + private final Map> storageIdsByType; public SignalStorageManifest(long version, List storageIds) { - this.version = version; - this.storageIds = storageIds; + this.version = version; + this.storageIds = storageIds; + this.storageIdsByType = new HashMap<>(); + + for (StorageId id : storageIds) { + List list = storageIdsByType.get(id.getType()); + if (list == null) { + list = new ArrayList<>(); + } + list.add(id); + storageIdsByType.put(id.getType(), list); + } } public long getVersion() { @@ -18,4 +37,14 @@ public class SignalStorageManifest { public List getStorageIds() { return storageIds; } + + public Optional getAccountStorageId() { + List list = storageIdsByType.get(ManifestRecord.Identifier.Type.ACCOUNT_VALUE); + + if (list.size() > 0) { + return Optional.of(list.get(0)); + } else { + return Optional.absent(); + } + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java index 971c062451..d390e808fc 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageModels.java @@ -2,14 +2,8 @@ package org.whispersystems.signalservice.api.storage; import com.google.protobuf.ByteString; -import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.signalservice.internal.storage.protos.ContactRecord; -import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record; -import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record; import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; import org.whispersystems.signalservice.internal.storage.protos.StorageItem; import org.whispersystems.signalservice.internal.storage.protos.StorageManifest; @@ -37,13 +31,16 @@ public final class SignalStorageModels { byte[] key = item.getKey().toByteArray(); byte[] rawRecord = SignalStorageCipher.decrypt(storageKey.deriveItemKey(key), item.getValue().toByteArray()); StorageRecord record = StorageRecord.parseFrom(rawRecord); + StorageId id = StorageId.forType(key, type); if (record.hasContact() && type == ManifestRecord.Identifier.Type.CONTACT_VALUE) { - return SignalStorageRecord.forContact(StorageId.forContact(key), remoteToLocalContactRecord(key, record.getContact())); + return SignalStorageRecord.forContact(id, new SignalContactRecord(id, record.getContact())); } else if (record.hasGroupV1() && type == ManifestRecord.Identifier.Type.GROUPV1_VALUE) { - return SignalStorageRecord.forGroupV1(StorageId.forGroupV1(key), remoteToLocalGroupV1Record(key, record.getGroupV1())); + return SignalStorageRecord.forGroupV1(id, new SignalGroupV1Record(id, record.getGroupV1())); } else if (record.hasGroupV2() && type == ManifestRecord.Identifier.Type.GROUPV2_VALUE && record.getGroupV2().getMasterKey().size() == GroupMasterKey.SIZE) { - return SignalStorageRecord.forGroupV2(StorageId.forGroupV2(key), remoteToLocalGroupV2Record(key, record.getGroupV2())); + return SignalStorageRecord.forGroupV2(id, new SignalGroupV2Record(id, record.getGroupV2())); + } else if (record.hasAccount() && type == ManifestRecord.Identifier.Type.ACCOUNT_VALUE) { + return SignalStorageRecord.forAccount(id, new SignalAccountRecord(id, record.getAccount())); } else { return SignalStorageRecord.forUnknown(StorageId.forType(key, type)); } @@ -58,6 +55,8 @@ public final class SignalStorageModels { builder.setGroupV1(record.getGroupV1().get().toProto()); } else if (record.getGroupV2().isPresent()) { builder.setGroupV2(record.getGroupV2().get().toProto()); + } else if (record.getAccount().isPresent()) { + builder.setAccount(record.getAccount().get().toProto()); } else { throw new InvalidStorageWriteError(); } @@ -72,42 +71,6 @@ public final class SignalStorageModels { .build(); } - private static SignalContactRecord remoteToLocalContactRecord(byte[] key, ContactRecord contact) { - SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164()); - - return new SignalContactRecord.Builder(key, address) - .setBlocked(contact.getBlocked()) - .setProfileSharingEnabled(contact.getWhitelisted()) - .setProfileKey(contact.getProfileKey().toByteArray()) - .setGivenName(contact.getGivenName()) - .setFamilyName(contact.getFamilyName()) - .setUsername(contact.getUsername()) - .setIdentityKey(contact.getIdentityKey().toByteArray()) - .setIdentityState(contact.getIdentityState()) - .setArchived(contact.getArchived()) - .build(); - } - - private static SignalGroupV1Record remoteToLocalGroupV1Record(byte[] key, GroupV1Record groupV1) { - return new SignalGroupV1Record.Builder(key, groupV1.getId().toByteArray()) - .setBlocked(groupV1.getBlocked()) - .setProfileSharingEnabled(groupV1.getWhitelisted()) - .setArchived(groupV1.getArchived()) - .build(); - } - - private static SignalGroupV2Record remoteToLocalGroupV2Record(byte[] key, GroupV2Record groupV2) { - try { - return new SignalGroupV2Record.Builder(key, new GroupMasterKey(groupV2.getMasterKey().toByteArray())) - .setBlocked(groupV2.getBlocked()) - .setProfileSharingEnabled(groupV2.getWhitelisted()) - .setArchived(groupV2.getArchived()) - .build(); - } catch (InvalidInputException e) { - throw new AssertionError(); - } - } - private static class InvalidStorageWriteError extends Error { } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java index 564f4357ad..d80a19eff7 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalStorageRecord.java @@ -1,6 +1,7 @@ package org.whispersystems.signalservice.api.storage; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import java.util.Objects; @@ -10,13 +11,14 @@ public class SignalStorageRecord implements SignalRecord { private final Optional contact; private final Optional groupV1; private final Optional groupV2; + private final Optional account; public static SignalStorageRecord forContact(SignalContactRecord contact) { return forContact(contact.getId(), contact); } public static SignalStorageRecord forContact(StorageId key, SignalContactRecord contact) { - return new SignalStorageRecord(key, Optional.of(contact), Optional.absent(), Optional.absent()); + return new SignalStorageRecord(key, Optional.of(contact), Optional.absent(), Optional.absent(), Optional.absent()); } public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) { @@ -24,7 +26,7 @@ public class SignalStorageRecord implements SignalRecord { } public static SignalStorageRecord forGroupV1(StorageId key, SignalGroupV1Record groupV1) { - return new SignalStorageRecord(key, Optional.absent(), Optional.of(groupV1), Optional.absent()); + return new SignalStorageRecord(key, Optional.absent(), Optional.of(groupV1), Optional.absent(), Optional.absent()); } public static SignalStorageRecord forGroupV2(SignalGroupV2Record groupV2) { @@ -32,22 +34,32 @@ public class SignalStorageRecord implements SignalRecord { } public static SignalStorageRecord forGroupV2(StorageId key, SignalGroupV2Record groupV2) { - return new SignalStorageRecord(key, Optional.absent(), Optional.absent(), Optional.of(groupV2)); + return new SignalStorageRecord(key, Optional.absent(), Optional.absent(), Optional.of(groupV2), Optional.absent()); + } + + public static SignalStorageRecord forAccount(SignalAccountRecord account) { + return forAccount(account.getId(), account); + } + + public static SignalStorageRecord forAccount(StorageId key, SignalAccountRecord account) { + return new SignalStorageRecord(key, Optional.absent(), Optional.absent(), Optional.absent(), Optional.of(account)); } public static SignalStorageRecord forUnknown(StorageId key) { - return new SignalStorageRecord(key,Optional.absent(), Optional.absent(), Optional.absent()); + return new SignalStorageRecord(key,Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent()); } private SignalStorageRecord(StorageId id, Optional contact, Optional groupV1, - Optional groupV2) + Optional groupV2, + Optional account) { this.id = id; this.contact = contact; this.groupV1 = groupV1; this.groupV2 = groupV2; + this.account = account; } @Override @@ -71,8 +83,12 @@ public class SignalStorageRecord implements SignalRecord { return groupV2; } + public Optional getAccount() { + return account; + } + public boolean isUnknown() { - return !contact.isPresent() && !groupV1.isPresent(); + return !contact.isPresent() && !groupV1.isPresent() && !account.isPresent(); } @Override diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/StorageId.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/StorageId.java index c300612a16..8819b8872b 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/StorageId.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/StorageId.java @@ -21,6 +21,10 @@ public class StorageId { return new StorageId(ManifestRecord.Identifier.Type.GROUPV2_VALUE, raw); } + public static StorageId forAccount(byte[] raw) { + return new StorageId(ManifestRecord.Identifier.Type.ACCOUNT_VALUE, raw); + } + public static StorageId forType(byte[] raw, int type) { return new StorageId(type, raw); } diff --git a/libsignal/service/src/main/proto/SignalStorage.proto b/libsignal/service/src/main/proto/SignalStorage.proto index de0a175373..e36d4273c7 100644 --- a/libsignal/service/src/main/proto/SignalStorage.proto +++ b/libsignal/service/src/main/proto/SignalStorage.proto @@ -97,13 +97,13 @@ message GroupV2Record { } message AccountRecord { - message Config { - bool readReceipts = 1; - bool sealedSenderIndicators = 2; - bool typingIndicators = 3; - bool linkPreviews = 4; - } - - ContactRecord contact = 1; - Config config = 2; + bytes profileKey = 1; + string givenName = 2; + string familyName = 3; + string avatarUrlPath = 4; + bool noteToSelfArchived = 5; + bool readReceipts = 6; + bool sealedSenderIndicators = 7; + bool typingIndicators = 8; + bool linkPreviews = 9; }