Search in sources :

Example 1 with StorageId

use of org.whispersystems.signalservice.api.storage.StorageId in project Signal-Android by WhisperSystems.

the class StorageSyncJob method performSync.

private boolean performSync() throws IOException, RetryLaterException, InvalidKeyException {
    final Stopwatch stopwatch = new Stopwatch("StorageSync");
    final SQLiteDatabase db = SignalDatabase.getRawDatabase();
    final SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
    final UnknownStorageIdDatabase storageIdDatabase = SignalDatabase.unknownStorageIds();
    final StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey();
    final SignalStorageManifest localManifest = SignalStore.storageService().getManifest();
    final SignalStorageManifest remoteManifest = accountManager.getStorageManifestIfDifferentVersion(storageServiceKey, localManifest.getVersion()).or(localManifest);
    stopwatch.split("remote-manifest");
    Recipient self = freshSelf();
    boolean needsMultiDeviceSync = false;
    boolean needsForcePush = false;
    if (self.getStorageServiceId() == null) {
        Log.w(TAG, "No storageId for self. Generating.");
        SignalDatabase.recipients().updateStorageId(self.getId(), StorageSyncHelper.generateKey());
        self = freshSelf();
    }
    Log.i(TAG, "Our version: " + localManifest.getVersion() + ", their version: " + remoteManifest.getVersion());
    if (remoteManifest.getVersion() > localManifest.getVersion()) {
        Log.i(TAG, "[Remote Sync] Newer manifest version found!");
        List<StorageId> localStorageIdsBeforeMerge = getAllLocalStorageIds(context, self);
        IdDifferenceResult idDifference = StorageSyncHelper.findIdDifference(remoteManifest.getStorageIds(), localStorageIdsBeforeMerge);
        if (idDifference.hasTypeMismatches() && SignalStore.account().isPrimaryDevice()) {
            Log.w(TAG, "[Remote Sync] Found type mismatches in the ID sets! Scheduling a force push after this sync completes.");
            needsForcePush = true;
        }
        Log.i(TAG, "[Remote Sync] Pre-Merge ID Difference :: " + idDifference);
        stopwatch.split("remote-id-diff");
        if (!idDifference.isEmpty()) {
            Log.i(TAG, "[Remote Sync] Retrieving records for key difference.");
            List<SignalStorageRecord> remoteOnly = accountManager.readStorageRecords(storageServiceKey, idDifference.getRemoteOnlyIds());
            stopwatch.split("remote-records");
            if (remoteOnly.size() != idDifference.getRemoteOnlyIds().size()) {
                Log.w(TAG, "[Remote Sync] Could not find all remote-only records! Requested: " + idDifference.getRemoteOnlyIds().size() + ", Found: " + remoteOnly.size() + ". These stragglers should naturally get deleted during the sync.");
            }
            List<SignalContactRecord> remoteContacts = new LinkedList<>();
            List<SignalGroupV1Record> remoteGv1 = new LinkedList<>();
            List<SignalGroupV2Record> remoteGv2 = new LinkedList<>();
            List<SignalAccountRecord> remoteAccount = new LinkedList<>();
            List<SignalStorageRecord> remoteUnknown = new LinkedList<>();
            for (SignalStorageRecord remote : remoteOnly) {
                if (remote.getContact().isPresent()) {
                    remoteContacts.add(remote.getContact().get());
                } else if (remote.getGroupV1().isPresent()) {
                    remoteGv1.add(remote.getGroupV1().get());
                } else if (remote.getGroupV2().isPresent()) {
                    remoteGv2.add(remote.getGroupV2().get());
                } else if (remote.getAccount().isPresent()) {
                    remoteAccount.add(remote.getAccount().get());
                } else if (remote.getId().isUnknown()) {
                    remoteUnknown.add(remote);
                } else {
                    Log.w(TAG, "Bad record! Type is a known value (" + remote.getId().getType() + "), but doesn't have a matching inner record. Dropping it.");
                }
            }
            db.beginTransaction();
            try {
                self = freshSelf();
                Log.i(TAG, "[Remote Sync] Remote-Only :: Contacts: " + remoteContacts.size() + ", GV1: " + remoteGv1.size() + ", GV2: " + remoteGv2.size() + ", Account: " + remoteAccount.size());
                new ContactRecordProcessor(context, self).process(remoteContacts, StorageSyncHelper.KEY_GENERATOR);
                new GroupV1RecordProcessor(context).process(remoteGv1, StorageSyncHelper.KEY_GENERATOR);
                new GroupV2RecordProcessor(context).process(remoteGv2, StorageSyncHelper.KEY_GENERATOR);
                self = freshSelf();
                new AccountRecordProcessor(context, self).process(remoteAccount, StorageSyncHelper.KEY_GENERATOR);
                List<SignalStorageRecord> unknownInserts = remoteUnknown;
                List<StorageId> unknownDeletes = Stream.of(idDifference.getLocalOnlyIds()).filter(StorageId::isUnknown).toList();
                Log.i(TAG, "[Remote Sync] Unknowns :: " + unknownInserts.size() + " inserts, " + unknownDeletes.size() + " deletes");
                storageIdDatabase.insert(unknownInserts);
                storageIdDatabase.delete(unknownDeletes);
                db.setTransactionSuccessful();
            } finally {
                db.endTransaction();
                ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
                stopwatch.split("remote-merge-transaction");
            }
        } else {
            Log.i(TAG, "[Remote Sync] Remote version was newer, but there were no remote-only IDs.");
        }
    } else if (remoteManifest.getVersion() < localManifest.getVersion()) {
        Log.w(TAG, "[Remote Sync] Remote version was older. User might have switched accounts.");
    }
    if (remoteManifest != localManifest) {
        Log.i(TAG, "[Remote Sync] Saved new manifest. Now at version: " + remoteManifest.getVersion());
        SignalStore.storageService().setManifest(remoteManifest);
    }
    Log.i(TAG, "We are up-to-date with the remote storage state.");
    final WriteOperationResult remoteWriteOperation;
    db.beginTransaction();
    try {
        self = freshSelf();
        List<StorageId> localStorageIds = getAllLocalStorageIds(context, self);
        IdDifferenceResult idDifference = StorageSyncHelper.findIdDifference(remoteManifest.getStorageIds(), localStorageIds);
        List<SignalStorageRecord> remoteInserts = buildLocalStorageRecords(context, self, idDifference.getLocalOnlyIds());
        List<byte[]> remoteDeletes = Stream.of(idDifference.getRemoteOnlyIds()).map(StorageId::getRaw).toList();
        Log.i(TAG, "ID Difference :: " + idDifference);
        remoteWriteOperation = new WriteOperationResult(new SignalStorageManifest(remoteManifest.getVersion() + 1, localStorageIds), remoteInserts, remoteDeletes);
        db.setTransactionSuccessful();
    } finally {
        db.endTransaction();
        stopwatch.split("local-data-transaction");
    }
    if (!remoteWriteOperation.isEmpty()) {
        Log.i(TAG, "We have something to write remotely.");
        Log.i(TAG, "WriteOperationResult :: " + remoteWriteOperation);
        StorageSyncValidations.validate(remoteWriteOperation, remoteManifest, needsForcePush, self);
        Optional<SignalStorageManifest> conflict = accountManager.writeStorageRecords(storageServiceKey, remoteWriteOperation.getManifest(), remoteWriteOperation.getInserts(), remoteWriteOperation.getDeletes());
        if (conflict.isPresent()) {
            Log.w(TAG, "Hit a conflict when trying to resolve the conflict! Retrying.");
            throw new RetryLaterException();
        }
        Log.i(TAG, "Saved new manifest. Now at version: " + remoteWriteOperation.getManifest().getVersion());
        SignalStore.storageService().setManifest(remoteWriteOperation.getManifest());
        stopwatch.split("remote-write");
        needsMultiDeviceSync = true;
    } else {
        Log.i(TAG, "No remote writes needed. Still at version: " + remoteManifest.getVersion());
    }
    if (needsForcePush && SignalStore.account().isPrimaryDevice()) {
        Log.w(TAG, "Scheduling a force push.");
        ApplicationDependencies.getJobManager().add(new StorageForcePushJob());
    }
    stopwatch.stop(TAG);
    return needsMultiDeviceSync;
}
Also used : SignalContactRecord(org.whispersystems.signalservice.api.storage.SignalContactRecord) Stopwatch(org.thoughtcrime.securesms.util.Stopwatch) StorageId(org.whispersystems.signalservice.api.storage.StorageId) GroupV2RecordProcessor(org.thoughtcrime.securesms.storage.GroupV2RecordProcessor) SignalGroupV2Record(org.whispersystems.signalservice.api.storage.SignalGroupV2Record) UnknownStorageIdDatabase(org.thoughtcrime.securesms.database.UnknownStorageIdDatabase) StorageKey(org.whispersystems.signalservice.api.storage.StorageKey) SignalAccountRecord(org.whispersystems.signalservice.api.storage.SignalAccountRecord) SignalStorageManifest(org.whispersystems.signalservice.api.storage.SignalStorageManifest) IdDifferenceResult(org.thoughtcrime.securesms.storage.StorageSyncHelper.IdDifferenceResult) GroupV1RecordProcessor(org.thoughtcrime.securesms.storage.GroupV1RecordProcessor) SignalServiceAccountManager(org.whispersystems.signalservice.api.SignalServiceAccountManager) SignalStorageRecord(org.whispersystems.signalservice.api.storage.SignalStorageRecord) Recipient(org.thoughtcrime.securesms.recipients.Recipient) WriteOperationResult(org.thoughtcrime.securesms.storage.StorageSyncHelper.WriteOperationResult) LinkedList(java.util.LinkedList) SignalGroupV1Record(org.whispersystems.signalservice.api.storage.SignalGroupV1Record) AccountRecordProcessor(org.thoughtcrime.securesms.storage.AccountRecordProcessor) SQLiteDatabase(net.zetetic.database.sqlcipher.SQLiteDatabase) ContactRecordProcessor(org.thoughtcrime.securesms.storage.ContactRecordProcessor) RetryLaterException(org.thoughtcrime.securesms.transport.RetryLaterException)

Example 2 with StorageId

use of org.whispersystems.signalservice.api.storage.StorageId in project Signal-Android by WhisperSystems.

the class StorageSyncJob method buildLocalStorageRecords.

@NonNull
private static List<SignalStorageRecord> buildLocalStorageRecords(@NonNull Context context, @NonNull Recipient self, @NonNull Collection<StorageId> ids) {
    if (ids.isEmpty()) {
        return Collections.emptyList();
    }
    RecipientDatabase recipientDatabase = SignalDatabase.recipients();
    UnknownStorageIdDatabase storageIdDatabase = SignalDatabase.unknownStorageIds();
    List<SignalStorageRecord> records = new ArrayList<>(ids.size());
    for (StorageId id : ids) {
        switch(id.getType()) {
            case ManifestRecord.Identifier.Type.CONTACT_VALUE:
            case ManifestRecord.Identifier.Type.GROUPV1_VALUE:
            case ManifestRecord.Identifier.Type.GROUPV2_VALUE:
                RecipientRecord settings = recipientDatabase.getByStorageId(id.getRaw());
                if (settings != null) {
                    if (settings.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 && settings.getSyncExtras().getGroupMasterKey() == null) {
                        throw new MissingGv2MasterKeyError();
                    } else {
                        records.add(StorageSyncModels.localToRemoteRecord(settings));
                    }
                } else {
                    throw new MissingRecipientModelError("Missing local recipient model! Type: " + id.getType());
                }
                break;
            case ManifestRecord.Identifier.Type.ACCOUNT_VALUE:
                if (!Arrays.equals(self.getStorageServiceId(), id.getRaw())) {
                    throw new AssertionError("Local storage ID doesn't match self!");
                }
                records.add(StorageSyncHelper.buildAccountRecord(context, self));
                break;
            default:
                SignalStorageRecord unknown = storageIdDatabase.getById(id.getRaw());
                if (unknown != null) {
                    records.add(unknown);
                } else {
                    throw new MissingUnknownModelError("Missing local unknown model! Type: " + id.getType());
                }
                break;
        }
    }
    return records;
}
Also used : RecipientDatabase(org.thoughtcrime.securesms.database.RecipientDatabase) RecipientRecord(org.thoughtcrime.securesms.database.model.RecipientRecord) ArrayList(java.util.ArrayList) SignalStorageRecord(org.whispersystems.signalservice.api.storage.SignalStorageRecord) UnknownStorageIdDatabase(org.thoughtcrime.securesms.database.UnknownStorageIdDatabase) StorageId(org.whispersystems.signalservice.api.storage.StorageId) NonNull(androidx.annotation.NonNull)

Example 3 with StorageId

use of org.whispersystems.signalservice.api.storage.StorageId in project Signal-Android by WhisperSystems.

the class UnknownStorageIdDatabase method delete.

public void delete(@NonNull Collection<StorageId> deletes) {
    SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
    String deleteQuery = STORAGE_ID + " = ?";
    Preconditions.checkArgument(db.inTransaction(), "Must be in a transaction!");
    for (StorageId id : deletes) {
        String[] args = SqlUtil.buildArgs(Base64.encodeBytes(id.getRaw()));
        db.delete(TABLE_NAME, deleteQuery, args);
    }
}
Also used : StorageId(org.whispersystems.signalservice.api.storage.StorageId)

Example 4 with StorageId

use of org.whispersystems.signalservice.api.storage.StorageId in project Signal-Android by WhisperSystems.

the class UnknownStorageIdDatabase method getAllUnknownIds.

public List<StorageId> getAllUnknownIds() {
    List<StorageId> keys = new ArrayList<>();
    String query = TYPE + " > ?";
    String[] args = SqlUtil.buildArgs(StorageId.largestKnownType());
    try (Cursor cursor = databaseHelper.getSignalReadableDatabase().query(TABLE_NAME, null, query, args, null, null, null)) {
        while (cursor != null && cursor.moveToNext()) {
            String keyEncoded = cursor.getString(cursor.getColumnIndexOrThrow(STORAGE_ID));
            int type = cursor.getInt(cursor.getColumnIndexOrThrow(TYPE));
            try {
                keys.add(StorageId.forType(Base64.decode(keyEncoded), type));
            } catch (IOException e) {
                throw new AssertionError(e);
            }
        }
    }
    return keys;
}
Also used : ArrayList(java.util.ArrayList) IOException(java.io.IOException) Cursor(android.database.Cursor) StorageId(org.whispersystems.signalservice.api.storage.StorageId)

Example 5 with StorageId

use of org.whispersystems.signalservice.api.storage.StorageId in project Signal-Android by WhisperSystems.

the class StorageSyncHelper method findIdDifference.

/**
 * Given a list of all the local and remote keys you know about, this will return a result telling
 * you which keys are exclusively remote and which are exclusively local.
 *
 * @param remoteIds All remote keys available.
 * @param localIds All local keys available.
 *
 * @return An object describing which keys are exclusive to the remote data set and which keys are
 *         exclusive to the local data set.
 */
@NonNull
public static IdDifferenceResult findIdDifference(@NonNull Collection<StorageId> remoteIds, @NonNull Collection<StorageId> localIds) {
    Map<String, StorageId> remoteByRawId = Stream.of(remoteIds).collect(Collectors.toMap(id -> Base64.encodeBytes(id.getRaw()), id -> id));
    Map<String, StorageId> localByRawId = Stream.of(localIds).collect(Collectors.toMap(id -> Base64.encodeBytes(id.getRaw()), id -> id));
    boolean hasTypeMismatch = remoteByRawId.size() != remoteIds.size() || localByRawId.size() != localIds.size();
    Set<String> remoteOnlyRawIds = SetUtil.difference(remoteByRawId.keySet(), localByRawId.keySet());
    Set<String> localOnlyRawIds = SetUtil.difference(localByRawId.keySet(), remoteByRawId.keySet());
    Set<String> sharedRawIds = SetUtil.intersection(localByRawId.keySet(), remoteByRawId.keySet());
    for (String rawId : sharedRawIds) {
        StorageId remote = Objects.requireNonNull(remoteByRawId.get(rawId));
        StorageId local = Objects.requireNonNull(localByRawId.get(rawId));
        if (remote.getType() != local.getType()) {
            remoteOnlyRawIds.remove(rawId);
            localOnlyRawIds.remove(rawId);
            hasTypeMismatch = true;
            Log.w(TAG, "Remote type " + remote.getType() + " did not match local type " + local.getType() + "!");
        }
    }
    List<StorageId> remoteOnlyKeys = Stream.of(remoteOnlyRawIds).map(remoteByRawId::get).toList();
    List<StorageId> localOnlyKeys = Stream.of(localOnlyRawIds).map(localByRawId::get).toList();
    return new IdDifferenceResult(remoteOnlyKeys, localOnlyKeys, hasTypeMismatch);
}
Also used : SignalStore(org.thoughtcrime.securesms.keyvalue.SignalStore) Context(android.content.Context) SignalAccountRecord(org.whispersystems.signalservice.api.storage.SignalAccountRecord) SignalDatabase(org.thoughtcrime.securesms.database.SignalDatabase) Stream(com.annimon.stream.Stream) Util(org.thoughtcrime.securesms.util.Util) NonNull(androidx.annotation.NonNull) RecipientDatabase(org.thoughtcrime.securesms.database.RecipientDatabase) TextSecurePreferences(org.thoughtcrime.securesms.util.TextSecurePreferences) Locale(java.util.Locale) Map(java.util.Map) Recipient(org.thoughtcrime.securesms.recipients.Recipient) SignalContactRecord(org.whispersystems.signalservice.api.storage.SignalContactRecord) SignalStorageManifest(org.whispersystems.signalservice.api.storage.SignalStorageManifest) StorageId(org.whispersystems.signalservice.api.storage.StorageId) RetrieveProfileAvatarJob(org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob) Entropy(org.thoughtcrime.securesms.payments.Entropy) Collectors(com.annimon.stream.Collectors) Base64(org.thoughtcrime.securesms.util.Base64) ApplicationDependencies(org.thoughtcrime.securesms.dependencies.ApplicationDependencies) Collection(java.util.Collection) PhoneNumberPrivacyValues(org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues) Subscriber(org.thoughtcrime.securesms.subscription.Subscriber) Set(java.util.Set) SetUtil(org.thoughtcrime.securesms.util.SetUtil) OptionalUtil(org.whispersystems.signalservice.api.util.OptionalUtil) Optional(org.whispersystems.libsignal.util.guava.Optional) Objects(java.util.Objects) TimeUnit(java.util.concurrent.TimeUnit) Log(org.signal.core.util.logging.Log) List(java.util.List) Nullable(androidx.annotation.Nullable) StorageSyncJob(org.thoughtcrime.securesms.jobs.StorageSyncJob) SignalStorageRecord(org.whispersystems.signalservice.api.storage.SignalStorageRecord) VisibleForTesting(androidx.annotation.VisibleForTesting) RecipientRecord(org.thoughtcrime.securesms.database.model.RecipientRecord) StorageId(org.whispersystems.signalservice.api.storage.StorageId) NonNull(androidx.annotation.NonNull)

Aggregations

StorageId (org.whispersystems.signalservice.api.storage.StorageId)11 SignalStorageRecord (org.whispersystems.signalservice.api.storage.SignalStorageRecord)8 SignalStorageManifest (org.whispersystems.signalservice.api.storage.SignalStorageManifest)6 ArrayList (java.util.ArrayList)5 Recipient (org.thoughtcrime.securesms.recipients.Recipient)5 NonNull (androidx.annotation.NonNull)4 RecipientDatabase (org.thoughtcrime.securesms.database.RecipientDatabase)4 SignalAccountRecord (org.whispersystems.signalservice.api.storage.SignalAccountRecord)4 Stream (com.annimon.stream.Stream)3 List (java.util.List)3 Log (org.signal.core.util.logging.Log)3 UnknownStorageIdDatabase (org.thoughtcrime.securesms.database.UnknownStorageIdDatabase)3 RecipientRecord (org.thoughtcrime.securesms.database.model.RecipientRecord)3 Collectors (com.annimon.stream.Collectors)2 ByteString (com.google.protobuf.ByteString)2 IOException (java.io.IOException)2 HashMap (java.util.HashMap)2 LinkedList (java.util.LinkedList)2 Locale (java.util.Locale)2 Map (java.util.Map)2