use of org.whispersystems.signalservice.api.storage.SignalAccountRecord 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;
}
use of org.whispersystems.signalservice.api.storage.SignalAccountRecord in project Signal-Android by WhisperSystems.
the class ThreadDatabase method applyStorageSyncUpdate.
public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalAccountRecord record) {
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
db.beginTransaction();
try {
applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived(), record.isNoteToSelfForcedUnread());
ContentValues clearPinnedValues = new ContentValues();
clearPinnedValues.put(PINNED, 0);
db.update(TABLE_NAME, clearPinnedValues, null, null);
int pinnedPosition = 1;
for (SignalAccountRecord.PinnedConversation pinned : record.getPinnedConversations()) {
ContentValues pinnedValues = new ContentValues();
pinnedValues.put(PINNED, pinnedPosition);
Recipient pinnedRecipient;
if (pinned.getContact().isPresent()) {
pinnedRecipient = Recipient.externalPush(pinned.getContact().get());
} else if (pinned.getGroupV1Id().isPresent()) {
try {
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v1(pinned.getGroupV1Id().get()));
} catch (BadGroupIdException e) {
Log.w(TAG, "Failed to parse pinned groupV1 ID!", e);
pinnedRecipient = null;
}
} else if (pinned.getGroupV2MasterKey().isPresent()) {
try {
pinnedRecipient = Recipient.externalGroupExact(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get())));
} catch (InvalidInputException e) {
Log.w(TAG, "Failed to parse pinned groupV2 master key!", e);
pinnedRecipient = null;
}
} else {
Log.w(TAG, "Empty pinned conversation on the AccountRecord?");
pinnedRecipient = null;
}
if (pinnedRecipient != null) {
db.update(TABLE_NAME, pinnedValues, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(pinnedRecipient.getId()));
}
pinnedPosition++;
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
notifyConversationListListeners();
}
use of org.whispersystems.signalservice.api.storage.SignalAccountRecord in project Signal-Android by WhisperSystems.
the class StorageSyncHelper method applyAccountStorageSyncUpdates.
public static void applyAccountStorageSyncUpdates(@NonNull Context context, @NonNull Recipient self, @NonNull SignalAccountRecord updatedRecord, boolean fetchProfile) {
SignalAccountRecord localRecord = buildAccountRecord(context, self).getAccount().get();
applyAccountStorageSyncUpdates(context, self, new StorageRecordUpdate<>(localRecord, updatedRecord), fetchProfile);
}
use of org.whispersystems.signalservice.api.storage.SignalAccountRecord in project Signal-Android by WhisperSystems.
the class StorageSyncHelper method buildAccountRecord.
public static SignalStorageRecord buildAccountRecord(@NonNull Context context, @NonNull Recipient self) {
RecipientDatabase recipientDatabase = SignalDatabase.recipients();
RecipientRecord record = recipientDatabase.getRecordForSync(self.getId());
List<RecipientRecord> pinned = Stream.of(SignalDatabase.threads().getPinnedRecipientIds()).map(recipientDatabase::getRecordForSync).toList();
SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId(), record != null ? record.getSyncExtras().getStorageProto() : null).setProfileKey(self.getProfileKey()).setGivenName(self.getProfileName().getGivenName()).setFamilyName(self.getProfileName().getFamilyName()).setAvatarUrlPath(self.getProfileAvatar()).setNoteToSelfArchived(record != null && record.getSyncExtras().isArchived()).setNoteToSelfForcedUnread(record != null && record.getSyncExtras().isForcedUnread()).setTypingIndicatorsEnabled(TextSecurePreferences.isTypingIndicatorsEnabled(context)).setReadReceiptsEnabled(TextSecurePreferences.isReadReceiptsEnabled(context)).setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)).setLinkPreviewsEnabled(SignalStore.settings().isLinkPreviewsEnabled()).setUnlistedPhoneNumber(SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isUnlisted()).setPhoneNumberSharingMode(StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode())).setPinnedConversations(StorageSyncModels.localToRemotePinnedConversations(pinned)).setPreferContactAvatars(SignalStore.settings().isPreferSystemContactPhotos()).setPayments(SignalStore.paymentsValues().mobileCoinPaymentsEnabled(), Optional.fromNullable(SignalStore.paymentsValues().getPaymentsEntropy()).transform(Entropy::getBytes).orNull()).setPrimarySendsSms(Util.isDefaultSmsProvider(context)).setUniversalExpireTimer(SignalStore.settings().getUniversalExpireTimer()).setE164(self.requireE164()).setDefaultReactions(SignalStore.emojiValues().getReactions()).setSubscriber(StorageSyncModels.localToRemoteSubscriber(SignalStore.donationsValues().getSubscriber())).setDisplayBadgesOnProfile(SignalStore.donationsValues().getDisplayBadgesOnProfile()).setSubscriptionManuallyCancelled(SignalStore.donationsValues().isUserManuallyCancelled()).build();
return SignalStorageRecord.forAccount(account);
}
use of org.whispersystems.signalservice.api.storage.SignalAccountRecord in project Signal-Android by WhisperSystems.
the class StorageAccountRestoreJob method onRun.
@Override
protected void onRun() throws Exception {
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey();
Log.i(TAG, "Retrieving manifest...");
Optional<SignalStorageManifest> manifest = accountManager.getStorageManifest(storageServiceKey);
if (!manifest.isPresent()) {
Log.w(TAG, "Manifest did not exist or was undecryptable (bad key). Not restoring. Force-pushing.");
ApplicationDependencies.getJobManager().add(new StorageForcePushJob());
return;
}
Log.i(TAG, "Resetting the local manifest to an empty state so that it will sync later.");
SignalStore.storageService().setManifest(SignalStorageManifest.EMPTY);
Optional<StorageId> accountId = manifest.get().getAccountStorageId();
if (!accountId.isPresent()) {
Log.w(TAG, "Manifest had no account record! Not restoring.");
return;
}
Log.i(TAG, "Retrieving account record...");
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;
}
Log.i(TAG, "Applying changes locally...");
SignalDatabase.getRawDatabase().beginTransaction();
try {
StorageSyncHelper.applyAccountStorageSyncUpdates(context, Recipient.self(), accountRecord, false);
SignalDatabase.getRawDatabase().setTransactionSuccessful();
} finally {
SignalDatabase.getRawDatabase().endTransaction();
}
JobManager jobManager = ApplicationDependencies.getJobManager();
if (accountRecord.getAvatarUrlPath().isPresent()) {
Log.i(TAG, "Fetching avatar...");
Optional<JobTracker.JobState> state = jobManager.runSynchronously(new RetrieveProfileAvatarJob(Recipient.self(), accountRecord.getAvatarUrlPath().get()), LIFESPAN / 2);
if (state.isPresent()) {
Log.i(TAG, "Avatar retrieved successfully. " + state.get());
} else {
Log.w(TAG, "Avatar retrieval did not complete in time (or otherwise failed).");
}
} else {
Log.i(TAG, "No avatar present. Not fetching.");
}
Log.i(TAG, "Refreshing attributes...");
Optional<JobTracker.JobState> state = jobManager.runSynchronously(new RefreshAttributesJob(), LIFESPAN / 2);
if (state.isPresent()) {
Log.i(TAG, "Attributes refreshed successfully. " + state.get());
} else {
Log.w(TAG, "Attribute refresh did not complete in time (or otherwise failed).");
}
}
Aggregations