Add storage support for the AccountRecord.

This commit is contained in:
Greyson Parrelli 2020-03-18 16:31:45 -04:00
parent 7a038ab09d
commit 951a61117a
38 changed files with 1290 additions and 335 deletions

View file

@ -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) {

View file

@ -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);
}

View file

@ -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;

View file

@ -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<RecipientSettings> 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<RecipientSettings> 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<RecipientSettings> 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<RecipientId, StorageId> 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<SignalGroupV1Record> 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<String, String> 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<StorageId> getAllStorageSyncKeys() {
return new ArrayList<>(getAllStorageSyncKeysMap().values());
public List<StorageId> 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<RecipientId, StorageId> 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<RecipientId, StorageId> 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<RecipientId, StorageId> 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() {

View file

@ -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<RecipientId, Boolean> status) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
db.beginTransaction();
try {
String query = RECIPIENT_ID + " = ?";
for (Map.Entry<RecipientId, Boolean> 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<RecipientId> getArchivedRecipients() {
Set<RecipientId> 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) {

View file

@ -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());

View file

@ -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

View file

@ -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.");

View file

@ -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<SignalStorageManifest> manifest = accountManager.getStorageManifest(storageServiceKey);
if (!manifest.isPresent()) {
Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring.");
return;
}
Optional<StorageId> accountId = manifest.get().getAccountStorageId();
if (!accountId.isPresent()) {
Log.w(TAG, "Manifest had no account record! Not restoring.");
return;
}
List<SignalStorageRecord> 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<StorageAccountRestoreJob> {
@Override
public @NonNull
StorageAccountRestoreJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new StorageAccountRestoreJob(parameters);
}
}
}

View file

@ -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<RecipientId, StorageId> oldStorageKeys = recipientDatabase.getAllStorageSyncKeysMap();
Map<RecipientId, StorageId> oldStorageKeys = recipientDatabase.getContactStorageSyncIdsMap();
long newVersion = currentVersion + 1;
Map<RecipientId, StorageId> newStorageKeys = generateNewKeys(oldStorageKeys);
List<SignalStorageRecord> 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<RecipientId, StorageId> newStorageKeys = generateNewKeys(oldStorageKeys);
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
List<SignalStorageRecord> 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()));

View file

@ -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<StorageId> allLocalStorageKeys = getAllLocalStorageKeys(context);
KeyDifferenceResult keyDifference = StorageSyncHelper.findKeyDifference(remoteManifest.get().getStorageIds(), allLocalStorageKeys);
List<StorageId> 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<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys());
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
List<SignalStorageRecord> localOnly = buildLocalStorageRecords(context, keyDifference.getLocalOnlyKeys(), archivedRecipients);
List<SignalStorageRecord> 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<StorageId> allLocalStorageKeys = recipientDatabase.getAllStorageSyncKeys();
List<RecipientSettings> pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates();
List<RecipientSettings> pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions();
List<RecipientSettings> pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions();
Optional<LocalWriteResult> localWriteResult = StorageSyncHelper.buildStorageUpdatesForLocal(localManifestVersion,
allLocalStorageKeys,
pendingUpdates,
pendingInsertions,
pendingDeletions);
List<StorageId> allLocalStorageKeys = getAllLocalStorageIds(context);
List<RecipientSettings> pendingUpdates = recipientDatabase.getPendingRecipientSyncUpdates();
List<RecipientSettings> pendingInsertions = recipientDatabase.getPendingRecipientSyncInsertions();
List<RecipientSettings> pendingDeletions = recipientDatabase.getPendingRecipientSyncDeletions();
Optional<SignalAccountRecord> pendingAccountUpdate = StorageSyncHelper.getPendingAccountSyncUpdate(context);
Optional<SignalAccountRecord> pendingAccountInsert = StorageSyncHelper.getPendingAccountSyncInsert(context);
Set<RecipientId> archivedRecipients = DatabaseFactory.getThreadDatabase(context).getArchivedRecipients();
Optional<LocalWriteResult> 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<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes());
Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, localWrite.getManifest(), localWrite.getInserts(), localWrite.getDeletes());
if (conflict.isPresent()) {
Log.w(TAG, "[Local Changes] Hit a conflict when trying to upload our local writes! Retrying.");
throw new RetryLaterException();
}
List<RecipientId> clearIds = new ArrayList<>(pendingUpdates.size() + pendingInsertions.size() + pendingDeletions.size());
List<RecipientId> clearIds = new ArrayList<>(pendingUpdates.size() + pendingInsertions.size() + pendingDeletions.size() + 1);
clearIds.addAll(Stream.of(pendingUpdates).map(RecipientSettings::getId).toList());
clearIds.addAll(Stream.of(pendingInsertions).map(RecipientSettings::getId).toList());
clearIds.addAll(Stream.of(pendingDeletions).map(RecipientSettings::getId).toList());
clearIds.add(Recipient.self().getId());
recipientDatabase.clearDirtyState(clearIds);
recipientDatabase.updateStorageKeys(localWriteResult.get().getStorageKeyUpdates());
@ -243,22 +248,44 @@ public class StorageSyncJob extends BaseJob {
return needsMultiDeviceSync;
}
private static @NonNull List<StorageId> getAllLocalStorageKeys(@NonNull Context context) {
return Util.concatenatedList(DatabaseFactory.getRecipientDatabase(context).getAllStorageSyncKeys(),
private static @NonNull List<StorageId> 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<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<StorageId> ids) {
private static @NonNull List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull List<StorageId> ids, @NonNull Set<RecipientId> archivedRecipients) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
StorageKeyDatabase storageKeyDatabase = DatabaseFactory.getStorageKeyDatabase(context);
List<SignalStorageRecord> 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;

View file

@ -18,7 +18,6 @@ public final class SignalStore {
public static void onFirstEverAppLaunch() {
registrationValues().onFirstEverAppLaunch();
storageServiceValues().setFirstStorageSyncCompleted(false);
}
public static @NonNull KbsValues kbsValues() {

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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() {

View file

@ -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;
}

View file

@ -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()));

View file

@ -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());

View file

@ -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<SignalAccountRecord> {
private static final String TAG = Log.tag(AccountConflictMerger.class);
private final Optional<SignalAccountRecord> local;
AccountConflictMerger(Optional<SignalAccountRecord> local) {
this.local = local;
}
@Override
public @NonNull Optional<SignalAccountRecord> getMatching(@NonNull SignalAccountRecord record) {
return local;
}
@Override
public @NonNull Collection<SignalAccountRecord> getInvalidEntries(@NonNull Collection<SignalAccountRecord> remoteRecords) {
Set<SignalAccountRecord> invalid = new HashSet<>(remoteRecords);
if (remoteRecords.size() > 0) {
invalid.remove(remoteRecords.iterator().next());
}
if (invalid.size() > 0) {
Log.w(TAG, "Found invalid account entries! Count: " + invalid.size());
}
return invalid;
}
@Override
public @NonNull SignalAccountRecord merge(@NonNull SignalAccountRecord remote, @NonNull SignalAccountRecord local, @NonNull 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;
}
}

View file

@ -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<SignalContactRecord> {
private static final String TAG = Log.tag(ContactConflictMerger.class);
private final Map<UUID, SignalContactRecord> localByUuid = new HashMap<>();
private final Map<String, SignalContactRecord> localByE164 = new HashMap<>();
ContactConflictMerger(@NonNull Collection<SignalContactRecord> localOnly) {
private final Recipient self;
ContactConflictMerger(@NonNull Collection<SignalContactRecord> localOnly, @NonNull Recipient self) {
for (SignalContactRecord contact : localOnly) {
if (contact.getAddress().getUuid().isPresent()) {
localByUuid.put(contact.getAddress().getUuid().get(), contact);
@ -29,6 +38,8 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
localByE164.put(contact.getAddress().getNumber().get(), contact);
}
}
this.self = self.resolve();
}
@Override
@ -39,6 +50,18 @@ class ContactConflictMerger implements StorageSyncHelper.ConflictMerger<SignalCo
return Optional.fromNullable(localUuid).or(Optional.fromNullable(localE164));
}
@Override
public @NonNull Collection<SignalContactRecord> getInvalidEntries(@NonNull Collection<SignalContactRecord> remoteRecords) {
List<SignalContactRecord> 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;

View file

@ -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<SignalGroupV1Record> {
@ -25,6 +26,11 @@ class GroupV1ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGr
return Optional.fromNullable(localByGroupId.get(GroupUtil.getEncodedId(record.getGroupId(), false)));
}
@Override
public @NonNull Collection<SignalGroupV1Record> getInvalidEntries(@NonNull Collection<SignalGroupV1Record> remoteRecords) {
return Collections.emptySet();
}
@Override
public @NonNull SignalGroupV1Record merge(@NonNull SignalGroupV1Record remote, @NonNull SignalGroupV1Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
boolean blocked = remote.isBlocked();

View file

@ -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<SignalGroupV2Record> {
@ -25,6 +26,11 @@ class GroupV2ConflictMerger implements StorageSyncHelper.ConflictMerger<SignalGr
return Optional.fromNullable(localByGroupId.get(record.getMasterKey()));
}
@Override
public @NonNull Collection<SignalGroupV2Record> getInvalidEntries(@NonNull Collection<SignalGroupV2Record> remoteRecords) {
return Collections.emptySet();
}
@Override
public @NonNull SignalGroupV2Record merge(@NonNull SignalGroupV2Record remote, @NonNull SignalGroupV2Record local, @NonNull StorageSyncHelper.KeyGenerator keyGenerator) {
boolean blocked = remote.isBlocked();

View file

@ -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<LocalWriteResult> buildStorageUpdatesForLocal(long currentManifestVersion,
@NonNull List<StorageId> currentLocalKeys,
@NonNull List<RecipientSettings> updates,
@NonNull List<RecipientSettings> inserts,
@NonNull List<RecipientSettings> deletes)
@NonNull List<RecipientSettings> deletes,
@NonNull Optional<SignalAccountRecord> accountUpdate,
@NonNull Optional<SignalAccountRecord> accountInsert,
@NonNull Set<RecipientId> archivedRecipients)
{
Set<StorageId> completeKeys = new LinkedHashSet<>(currentLocalKeys);
Set<SignalStorageRecord> storageInserts = new LinkedHashSet<>();
@ -68,33 +89,48 @@ public final class StorageSyncHelper {
Map<RecipientId, byte[]> 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<byte[]> contactDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList();
List<StorageId> completeKeysBytes = new ArrayList<>(completeKeys);
SignalStorageManifest manifest = new SignalStorageManifest(currentManifestVersion + 1, completeKeysBytes);
WriteOperationResult writeOperationResult = new WriteOperationResult(manifest, new ArrayList<>(storageInserts), contactDeleteBytes);
List<byte[]> contactDeleteBytes = Stream.of(storageDeletes).map(ByteBuffer::array).toList();
List<StorageId> 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<StorageId> remoteKeys,
@NonNull List<StorageId> localKeys)
public static @NonNull KeyDifferenceResult findKeyDifference(@NonNull Collection<StorageId> remoteKeys,
@NonNull Collection<StorageId> localKeys)
{
Set<StorageId> remoteOnlyKeys = SetUtil.difference(remoteKeys, localKeys);
Set<StorageId> localOnlyKeys = SetUtil.difference(localKeys, remoteKeys);
@ -143,12 +179,23 @@ public final class StorageSyncHelper {
List<SignalStorageRecord> remoteOnlyUnknowns = Stream.of(remoteOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
List<SignalStorageRecord> localOnlyUnknowns = Stream.of(localOnlyRecords).filter(r -> r.isUnknown() || r.getGroupV2().isPresent()).toList();
RecordMergeResult<SignalContactRecord, RecordUpdate<SignalContactRecord>> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts));
RecordMergeResult<SignalGroupV1Record, RecordUpdate<SignalGroupV1Record>> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1));
List<SignalAccountRecord> remoteOnlyAccount = Stream.of(remoteOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
List<SignalAccountRecord> localOnlyAccount = Stream.of(localOnlyRecords).filter(r -> r.getAccount().isPresent()).map(r -> r.getAccount().get()).toList();
if (remoteOnlyAccount.size() > 0 && localOnlyAccount.isEmpty()) {
throw new AssertionError("Found a remote-only account, but no local-only account!");
}
if (localOnlyAccount.size() > 1) {
throw new AssertionError("Multiple local accounts?");
}
RecordMergeResult<SignalContactRecord> contactMergeResult = resolveRecordConflict(remoteOnlyContacts, localOnlyContacts, new ContactConflictMerger(localOnlyContacts, Recipient.self()));
RecordMergeResult<SignalGroupV1Record> groupV1MergeResult = resolveRecordConflict(remoteOnlyGroupV1, localOnlyGroupV1, new GroupV1ConflictMerger(localOnlyGroupV1));
RecordMergeResult<SignalAccountRecord> accountMergeResult = resolveRecordConflict(remoteOnlyAccount, localOnlyAccount, new AccountConflictMerger(localOnlyAccount.isEmpty() ? Optional.absent() : Optional.of(localOnlyAccount.get(0))));
Set<SignalStorageRecord> remoteInserts = new HashSet<>();
remoteInserts.addAll(Stream.of(contactMergeResult.remoteInserts).map(SignalStorageRecord::forContact).toList());
remoteInserts.addAll(Stream.of(groupV1MergeResult.remoteInserts).map(SignalStorageRecord::forGroupV1).toList());
remoteInserts.addAll(Stream.of(accountMergeResult.remoteInserts).map(SignalStorageRecord::forAccount).toList());
Set<RecordUpdate<SignalStorageRecord>> 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<SignalRecord> 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<byte[]> 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 <E extends SignalRecord> RecordMergeResult<E, RecordUpdate<E>> resolveRecordConflict(@NonNull Collection<E> remoteOnlyRecords,
@NonNull Collection<E> localOnlyRecords,
@NonNull ConflictMerger<E> merger)
private static @NonNull <E extends SignalRecord> RecordMergeResult<E> resolveRecordConflict(@NonNull Collection<E> remoteOnlyRecords,
@NonNull Collection<E> localOnlyRecords,
@NonNull ConflictMerger<E> merger)
{
Set<E> localInserts = new LinkedHashSet<>(remoteOnlyRecords);
Set<E> remoteInserts = new LinkedHashSet<>(localOnlyRecords);
Set<RecordUpdate<E>> localUpdates = new LinkedHashSet<>();
Set<RecordUpdate<E>> remoteUpdates = new LinkedHashSet<>();
Set<E> localInserts = new HashSet<>(remoteOnlyRecords);
Set<E> remoteInserts = new HashSet<>(localOnlyRecords);
Set<RecordUpdate<E>> localUpdates = new HashSet<>();
Set<RecordUpdate<E>> remoteUpdates = new HashSet<>();
Set<E> remoteDeletes = new HashSet<>(merger.getInvalidEntries(remoteOnlyRecords));
remoteOnlyRecords.removeAll(remoteDeletes);
localInserts.removeAll(remoteDeletes);
for (E remote : remoteOnlyRecords) {
Optional<E> 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<SignalContactRecord> update) {
return !OptionalUtil.byteArrayEquals(update.getOld().getProfileKey(), update.getNew().getProfileKey());
}
public static Optional<SignalAccountRecord> 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<SignalAccountRecord> 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<StorageSyncHelper.RecordUpdate<SignalAccountRecord>> 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<StorageId> remoteOnlyKeys;
private final List<StorageId> localOnlyKeys;
@ -261,24 +394,28 @@ public final class StorageSyncHelper {
}
public static final class MergeResult {
private final Set<SignalContactRecord> localContactInserts;
private final Set<RecordUpdate<SignalContactRecord>> localContactUpdates;
private final Set<SignalGroupV1Record> localGroupV1Inserts;
private final Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
private final Set<SignalStorageRecord> localUnknownInserts;
private final Set<SignalStorageRecord> localUnknownDeletes;
private final Set<SignalStorageRecord> remoteInserts;
private final Set<RecordUpdate<SignalStorageRecord>> remoteUpdates;
private final Set<SignalContactRecord> localContactInserts;
private final Set<RecordUpdate<SignalContactRecord>> localContactUpdates;
private final Set<SignalGroupV1Record> localGroupV1Inserts;
private final Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates;
private final Set<SignalStorageRecord> localUnknownInserts;
private final Set<SignalStorageRecord> localUnknownDeletes;
private final Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate;
private final Set<SignalStorageRecord> remoteInserts;
private final Set<RecordUpdate<SignalStorageRecord>> remoteUpdates;
private final Set<SignalRecord> remoteDeletes;
@VisibleForTesting
MergeResult(@NonNull Set<SignalContactRecord> localContactInserts,
@NonNull Set<RecordUpdate<SignalContactRecord>> localContactUpdates,
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
@NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
@NonNull Set<SignalStorageRecord> localUnknownInserts,
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
@NonNull Set<SignalStorageRecord> remoteInserts,
@NonNull Set<RecordUpdate<SignalStorageRecord>> remoteUpdates)
MergeResult(@NonNull Set<SignalContactRecord> localContactInserts,
@NonNull Set<RecordUpdate<SignalContactRecord>> localContactUpdates,
@NonNull Set<SignalGroupV1Record> localGroupV1Inserts,
@NonNull Set<RecordUpdate<SignalGroupV1Record>> localGroupV1Updates,
@NonNull Set<SignalStorageRecord> localUnknownInserts,
@NonNull Set<SignalStorageRecord> localUnknownDeletes,
@NonNull Optional<RecordUpdate<SignalAccountRecord>> localAccountUpdate,
@NonNull Set<SignalStorageRecord> remoteInserts,
@NonNull Set<RecordUpdate<SignalStorageRecord>> remoteUpdates,
@NonNull Set<SignalRecord> remoteDeletes)
{
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<SignalContactRecord> getLocalContactInserts() {
@ -314,6 +453,10 @@ public final class StorageSyncHelper {
return localUnknownDeletes;
}
public @NonNull Optional<RecordUpdate<SignalAccountRecord>> getLocalAccountUpdate() {
return localAccountUpdate;
}
public @NonNull Set<SignalStorageRecord> getRemoteInserts() {
return remoteInserts;
}
@ -322,6 +465,10 @@ public final class StorageSyncHelper {
return remoteUpdates;
}
public @NonNull Set<SignalRecord> getRemoteDeletes() {
return remoteDeletes;
}
@NonNull Set<SignalRecord> getAllNewRecords() {
Set<SignalRecord> 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<Record, Update> {
final Set<Record> localInserts;
final Set<Update> localUpdates;
final Set<Record> remoteInserts;
final Set<Update> remoteUpdates;
private static class RecordMergeResult<Record extends SignalRecord> {
final Set<Record> localInserts;
final Set<RecordUpdate<Record>> localUpdates;
final Set<Record> remoteInserts;
final Set<RecordUpdate<Record>> remoteUpdates;
final Set<Record> remoteDeletes;
RecordMergeResult(@NonNull Set<Record> localInserts,
@NonNull Set<Update> localUpdates,
@NonNull Set<Record> remoteInserts,
@NonNull Set<Update> remoteUpdates)
RecordMergeResult(@NonNull Set<Record> localInserts,
@NonNull Set<RecordUpdate<Record>> localUpdates,
@NonNull Set<Record> remoteInserts,
@NonNull Set<RecordUpdate<Record>> remoteUpdates,
@NonNull Set<Record> remoteDeletes)
{
this.localInserts = localInserts;
this.localUpdates = localUpdates;
this.remoteInserts = remoteInserts;
this.remoteUpdates = remoteUpdates;
this.remoteDeletes = remoteDeletes;
}
}
interface ConflictMerger<E extends SignalRecord> {
@NonNull Optional<E> getMatching(@NonNull E record);
@NonNull Collection<E> getInvalidEntries(@NonNull Collection<E> remoteRecords);
@NonNull E merge(@NonNull E remote, @NonNull E local, @NonNull KeyGenerator keyGenerator);
}

View file

@ -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<RecipientId> 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<RecipientId> 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<RecipientId> 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<RecipientId> 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();
}

View file

@ -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<StorageId> allSet = new HashSet<>(result.getManifest().getStorageIds());
Set<StorageId> 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<String> 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 {
}
}

View file

@ -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 <T> List<T> concatenatedList(List<T> first, List<T> second) {
final List<T> concat = new ArrayList<>(first.size() + second.size());
@SafeVarargs
public static <T> List<T> concatenatedList(Collection <T>... items) {
final List<T> concat = new ArrayList<>(Stream.of(items).reduce(0, (sum, list) -> sum + list.size()));
concat.addAll(first);
concat.addAll(second);
for (Collection<T> list : items) {
concat.addAll(list);
}
return concat;
}

View file

@ -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<SignalContactRecord> invalid = new ContactConflictMerger(Collections.emptyList(), SELF).getInvalidEntries(setOf(a, b, self));
assertContentsEqual(setOf(self), invalid);
}
}

View file

@ -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<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, unknownRemote);
Set<SignalStorageRecord> localOnly = recordSetOf(local1, local2, local3, local4, unknownLocal);
Set<SignalStorageRecord> remoteOnly = recordSetOf(remote1, remote2, remote3, remote4, remote5, unknownRemote);
Set<SignalStorageRecord> 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<StorageId> 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<StorageId> 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<SignalStorageRecord> recordSetOf(SignalRecord... records) {
LinkedHashSet<SignalStorageRecord> 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<SignalStorageRecord> recordSetOf(SignalGroupV1Record... groupRecords) {
LinkedHashSet<SignalStorageRecord> 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<SignalContactRecord> contactUpdate(SignalContactRecord oldContact, SignalContactRecord newContact) {
return new StorageSyncHelper.RecordUpdate<>(oldContact, newContact);
private static <E extends SignalRecord> StorageSyncHelper.RecordUpdate<E> update(E oldRecord, E newRecord) {
return new StorageSyncHelper.RecordUpdate<>(oldRecord, newRecord);
}
private static StorageSyncHelper.RecordUpdate<SignalGroupV1Record> 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 <E extends SignalRecord> StorageSyncHelper.RecordUpdate<SignalStorageRecord> recordUpdate(E oldContact, E newContact) {
return new StorageSyncHelper.RecordUpdate<>(record(oldContact), record(newContact));
}
private static SignalStorageRecord unknown(int key) {

View file

@ -403,6 +403,18 @@ public class SignalServiceAccountManager {
}
}
public Optional<SignalStorageManifest> 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<SignalStorageRecord> readStorageRecords(StorageKey storageKey, List<StorageId> storageKeys) throws IOException, InvalidKeyException {
if (storageKeys.isEmpty()) {
return Collections.emptyList();
}
List<SignalStorageRecord> result = new ArrayList<>();
ReadOperation.Builder operation = ReadOperation.newBuilder();
Map<ByteString, Integer> typeMap = new HashMap<>();

View file

@ -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<String> givenName;
private final Optional<String> familyName;
private final Optional<String> avatarUrlPath;
private final Optional<byte[]> 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<String> getGivenName() {
return givenName;
}
public Optional<String> getFamilyName() {
return familyName;
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public Optional<String> 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());
}
}
}

View file

@ -23,7 +23,7 @@ public final class SignalContactRecord implements SignalRecord {
private final Optional<String> username;
private final Optional<byte[]> identityKey;
private SignalContactRecord(StorageId id, ContactRecord proto) {
public SignalContactRecord(StorageId id, ContactRecord proto) {
this.id = id;
this.proto = proto;

View file

@ -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();

View file

@ -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 {

View file

@ -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<StorageId> storageIds;
private final long version;
private final List<StorageId> storageIds;
private final Map<Integer, List<StorageId>> storageIdsByType;
public SignalStorageManifest(long version, List<StorageId> storageIds) {
this.version = version;
this.storageIds = storageIds;
this.version = version;
this.storageIds = storageIds;
this.storageIdsByType = new HashMap<>();
for (StorageId id : storageIds) {
List<StorageId> 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<StorageId> getStorageIds() {
return storageIds;
}
public Optional<StorageId> getAccountStorageId() {
List<StorageId> list = storageIdsByType.get(ManifestRecord.Identifier.Type.ACCOUNT_VALUE);
if (list.size() > 0) {
return Optional.of(list.get(0));
} else {
return Optional.absent();
}
}
}

View file

@ -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 {
}
}

View file

@ -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<SignalContactRecord> contact;
private final Optional<SignalGroupV1Record> groupV1;
private final Optional<SignalGroupV2Record> groupV2;
private final Optional<SignalAccountRecord> 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.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent());
return new SignalStorageRecord(key, Optional.of(contact), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent(), Optional.<SignalAccountRecord>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.<SignalContactRecord>absent(), Optional.of(groupV1), Optional.<SignalGroupV2Record>absent());
return new SignalStorageRecord(key, Optional.<SignalContactRecord>absent(), Optional.of(groupV1), Optional.<SignalGroupV2Record>absent(), Optional.<SignalAccountRecord>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.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.of(groupV2));
return new SignalStorageRecord(key, Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.of(groupV2), Optional.<SignalAccountRecord>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.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent(), Optional.of(account));
}
public static SignalStorageRecord forUnknown(StorageId key) {
return new SignalStorageRecord(key,Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent());
return new SignalStorageRecord(key,Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent(), Optional.<SignalGroupV2Record>absent(), Optional.<SignalAccountRecord>absent());
}
private SignalStorageRecord(StorageId id,
Optional<SignalContactRecord> contact,
Optional<SignalGroupV1Record> groupV1,
Optional<SignalGroupV2Record> groupV2)
Optional<SignalGroupV2Record> groupV2,
Optional<SignalAccountRecord> 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<SignalAccountRecord> getAccount() {
return account;
}
public boolean isUnknown() {
return !contact.isPresent() && !groupV1.isPresent();
return !contact.isPresent() && !groupV1.isPresent() && !account.isPresent();
}
@Override

View file

@ -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);
}

View file

@ -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;
}