diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java deleted file mode 100644 index ec78f4f7c4..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.thoughtcrime.securesms.jobs; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.annimon.stream.Stream; - -import org.signal.core.util.logging.Log; -import org.signal.libsignal.protocol.InvalidKeyException; -import org.thoughtcrime.securesms.database.RecipientTable; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.database.UnknownStorageIdTable; -import org.thoughtcrime.securesms.dependencies.AppDependencies; -import org.thoughtcrime.securesms.jobmanager.Job; -import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; -import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.storage.StorageSyncHelper; -import org.thoughtcrime.securesms.storage.StorageSyncModels; -import org.thoughtcrime.securesms.storage.StorageSyncValidations; -import org.thoughtcrime.securesms.transport.RetryLaterException; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -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.api.storage.StorageId; -import org.whispersystems.signalservice.api.storage.StorageKey; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.TimeUnit; - -/** - * Forces remote storage to match our local state. This should only be done when we detect that the - * remote data is badly-encrypted (which should only happen after re-registering without a PIN). - */ -public class StorageForcePushJob extends BaseJob { - - public static final String KEY = "StorageForcePushJob"; - - private static final String TAG = Log.tag(StorageForcePushJob.class); - - public StorageForcePushJob() { - this(new Parameters.Builder().addConstraint(NetworkConstraint.KEY) - .setQueue(StorageSyncJob.QUEUE_KEY) - .setMaxInstancesForFactory(1) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .build()); - } - - private StorageForcePushJob(@NonNull Parameters parameters) { - super(parameters); - } - - @Override - public @Nullable byte[] serialize() { - return null; - } - - @Override - public @NonNull String getFactoryKey() { - return KEY; - } - - @Override - protected void onRun() throws IOException, RetryLaterException { - if (SignalStore.account().isLinkedDevice()) { - Log.i(TAG, "Only the primary device can force push"); - return; - } - - if (!SignalStore.account().isRegistered() || SignalStore.account().getE164() == null) { - Log.w(TAG, "User not registered. Skipping."); - return; - } - - if (Recipient.self().getStorageId() == null) { - Log.w(TAG, "No storage ID set for self! Skipping."); - return; - } - - StorageKey storageServiceKey = SignalStore.storageService().getOrCreateStorageKey(); - SignalServiceAccountManager accountManager = AppDependencies.getSignalServiceAccountManager(); - RecipientTable recipientTable = SignalDatabase.recipients(); - UnknownStorageIdTable storageIdTable = SignalDatabase.unknownStorageIds(); - - long currentVersion = accountManager.getStorageManifestVersion(); - Map oldContactStorageIds = recipientTable.getContactStorageSyncIdsMap(); - - long newVersion = currentVersion + 1; - Map newContactStorageIds = generateContactStorageIds(oldContactStorageIds); - List inserts = Stream.of(oldContactStorageIds.keySet()) - .map(recipientTable::getRecordForSync) - .withoutNulls() - .map(s -> StorageSyncModels.localToRemoteRecord(s, Objects.requireNonNull(newContactStorageIds.get(s.getId())).getRaw())) - .toList(); - - SignalStorageRecord accountRecord = StorageSyncHelper.buildAccountRecord(context, Recipient.self().fresh()); - List allNewStorageIds = new ArrayList<>(newContactStorageIds.values()); - - inserts.add(accountRecord); - allNewStorageIds.add(accountRecord.getId()); - - SignalStorageManifest manifest = new SignalStorageManifest(newVersion, SignalStore.account().getDeviceId(), allNewStorageIds); - StorageSyncValidations.validateForcePush(manifest, inserts, Recipient.self().fresh()); - - try { - if (newVersion > 1) { - Log.i(TAG, String.format(Locale.ENGLISH, "Force-pushing data. Inserting %d IDs.", inserts.size())); - if (accountManager.resetStorageRecords(storageServiceKey, manifest, inserts).isPresent()) { - Log.w(TAG, "Hit a conflict. Trying again."); - throw new RetryLaterException(); - } - } else { - Log.i(TAG, String.format(Locale.ENGLISH, "First version, normal push. Inserting %d IDs.", inserts.size())); - if (accountManager.writeStorageRecords(storageServiceKey, manifest, inserts, Collections.emptyList()).isPresent()) { - Log.w(TAG, "Hit a conflict. Trying again."); - throw new RetryLaterException(); - } - } - } catch (InvalidKeyException e) { - Log.w(TAG, "Hit an invalid key exception, which likely indicates a conflict."); - throw new RetryLaterException(e); - } - - Log.i(TAG, "Force push succeeded. Updating local manifest version to: " + newVersion); - SignalStore.storageService().setManifest(manifest); - recipientTable.applyStorageIdUpdates(newContactStorageIds); - recipientTable.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().getId(), accountRecord.getId())); - storageIdTable.deleteAll(); - } - - @Override - protected boolean onShouldRetry(@NonNull Exception e) { - return e instanceof PushNetworkException || e instanceof RetryLaterException; - } - - @Override - public void onFailure() { - } - - private static @NonNull Map generateContactStorageIds(@NonNull Map oldKeys) { - Map out = new HashMap<>(); - - for (Map.Entry entry : oldKeys.entrySet()) { - out.put(entry.getKey(), entry.getValue().withNewBytes(StorageSyncHelper.generateKey())); - } - - return out; - } - - public static final class Factory implements Job.Factory { - @Override - public @NonNull StorageForcePushJob create(@NonNull Parameters parameters, @Nullable byte[] serializedData) { - return new StorageForcePushJob(parameters); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.kt new file mode 100644 index 0000000000..2b405293cf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/StorageForcePushJob.kt @@ -0,0 +1,133 @@ +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.signal.libsignal.protocol.InvalidKeyException +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.storage.StorageSyncModels +import org.thoughtcrime.securesms.storage.StorageSyncValidations +import org.thoughtcrime.securesms.transport.RetryLaterException +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.api.storage.StorageId +import java.io.IOException +import java.util.Collections +import java.util.concurrent.TimeUnit + +/** + * Forces remote storage to match our local state. This should only be done when we detect that the + * remote data is badly-encrypted (which should only happen after re-registering without a PIN). + */ +class StorageForcePushJob private constructor(parameters: Parameters) : BaseJob(parameters) { + companion object { + const val KEY: String = "StorageForcePushJob" + + private val TAG = Log.tag(StorageForcePushJob::class.java) + } + + constructor() : this( + Parameters.Builder().addConstraint(NetworkConstraint.KEY) + .setQueue(StorageSyncJob.QUEUE_KEY) + .setMaxInstancesForFactory(1) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .build() + ) + + override fun serialize(): ByteArray? = null + + override fun getFactoryKey(): String = KEY + + @Throws(IOException::class, RetryLaterException::class) + override fun onRun() { + if (SignalStore.account.isLinkedDevice) { + Log.i(TAG, "Only the primary device can force push") + return + } + + if (!SignalStore.account.isRegistered || SignalStore.account.e164 == null) { + Log.w(TAG, "User not registered. Skipping.") + return + } + + if (Recipient.self().storageId == null) { + Log.w(TAG, "No storage ID set for self! Skipping.") + return + } + + val storageServiceKey = SignalStore.storageService.getOrCreateStorageKey() + val accountManager = AppDependencies.signalServiceAccountManager + + val currentVersion = accountManager.storageManifestVersion + val oldContactStorageIds: Map = SignalDatabase.recipients.getContactStorageSyncIdsMap() + + val newVersion = currentVersion + 1 + val newContactStorageIds = generateContactStorageIds(oldContactStorageIds) + val inserts: MutableList = oldContactStorageIds.keys + .mapNotNull { SignalDatabase.recipients.getRecordForSync(it) } + .map { record -> StorageSyncModels.localToRemoteRecord(record, newContactStorageIds[record.id]!!.raw) } + .toMutableList() + + val accountRecord = StorageSyncHelper.buildAccountRecord(context, Recipient.self().fresh()) + val allNewStorageIds: MutableList = ArrayList(newContactStorageIds.values) + + inserts.add(accountRecord) + allNewStorageIds.add(accountRecord.id) + + val manifest = SignalStorageManifest(newVersion, SignalStore.account.deviceId, allNewStorageIds) + StorageSyncValidations.validateForcePush(manifest, inserts, Recipient.self().fresh()) + + try { + if (newVersion > 1) { + Log.i(TAG, "Force-pushing data. Inserting ${inserts.size} IDs.") + if (accountManager.resetStorageRecords(storageServiceKey, manifest, inserts).isPresent) { + Log.w(TAG, "Hit a conflict. Trying again.") + throw RetryLaterException() + } + } else { + Log.i(TAG, "First version, normal push. Inserting ${inserts.size} IDs.") + if (accountManager.writeStorageRecords(storageServiceKey, manifest, inserts, emptyList()).isPresent) { + Log.w(TAG, "Hit a conflict. Trying again.") + throw RetryLaterException() + } + } + } catch (e: InvalidKeyException) { + Log.w(TAG, "Hit an invalid key exception, which likely indicates a conflict.") + throw RetryLaterException(e) + } + + Log.i(TAG, "Force push succeeded. Updating local manifest version to: $newVersion") + SignalStore.storageService.manifest = manifest + SignalDatabase.recipients.applyStorageIdUpdates(newContactStorageIds) + SignalDatabase.recipients.applyStorageIdUpdates(Collections.singletonMap(Recipient.self().id, accountRecord.id)) + SignalDatabase.unknownStorageIds.deleteAll() + } + + override fun onShouldRetry(e: Exception): Boolean { + return e is PushNetworkException || e is RetryLaterException + } + + override fun onFailure() = Unit + + private fun generateContactStorageIds(oldKeys: Map): Map { + val out: MutableMap = mutableMapOf() + + for ((key, value) in oldKeys) { + out[key] = value.withNewBytes(StorageSyncHelper.generateKey()) + } + + return out + } + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): StorageForcePushJob { + return StorageForcePushJob(parameters) + } + } +}