From 5b543c5212a17b6153f06b5d065a078e0cd48c4b Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 5 Mar 2026 11:46:38 -0500 Subject: [PATCH] Fix release channel recipient ID surviving failed backup imports. --- .../securesms/backup/v2/BackupRepository.kt | 7 ++++ .../securesms/jobs/CreateReleaseChannelJob.kt | 37 ++++++++++-------- .../securesms/jobs/JobManagerFactories.java | 2 + .../keyvalue/ReleaseChannelValues.kt | 4 ++ .../migrations/ApplicationMigrations.java | 7 +++- .../ReleaseChannelRecipientFixMigrationJob.kt | 38 +++++++++++++++++++ 6 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/migrations/ReleaseChannelRecipientFixMigrationJob.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 5118d587bc..08e9ac506a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -1218,6 +1218,7 @@ object BackupRepository { } SignalStore.backup.hasInvalidBackupVersion = false + var transactionSuccessful = false try { // Removing all the data from the various tables is *very* expensive (i.e. can take *several* minutes) if we don't do some pre-work. // SQLite optimizes deletes if there's no foreign keys, triggers, or WHERE clause, so that's the environment we're gonna create. @@ -1406,11 +1407,17 @@ object BackupRepository { stopwatch.split("fk-check") SignalDatabase.rawDatabase.setTransactionSuccessful() + transactionSuccessful = true } finally { if (SignalDatabase.rawDatabase.inTransaction()) { SignalDatabase.rawDatabase.endTransaction() } + if (!transactionSuccessful) { + Log.w(TAG, "[import] Transaction failed, clearing release channel recipient ID from key-value store.") + SignalStore.releaseChannel.clearReleaseChannelRecipientId() + } + Log.d(TAG, "[import] Re-enabling foreign keys...") SignalDatabase.rawDatabase.forceForeignKeyConstraintsEnabled(true) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateReleaseChannelJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateReleaseChannelJob.kt index 89a0c766fc..a2ac1f0863 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateReleaseChannelJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/CreateReleaseChannelJob.kt @@ -51,24 +51,31 @@ class CreateReleaseChannelJob private constructor(parameters: Parameters) : Base } if (SignalStore.releaseChannel.releaseChannelRecipientId != null) { - Log.i(TAG, "Already created Release Channel recipient ${SignalStore.releaseChannel.releaseChannelRecipientId}") + val existingId = SignalStore.releaseChannel.releaseChannelRecipientId!! + val recipient = Recipient.resolved(existingId) - val recipient = Recipient.resolved(SignalStore.releaseChannel.releaseChannelRecipientId!!) - if (recipient.profileAvatar.isNullOrEmpty() || !SignalStore.releaseChannel.hasUpdatedAvatar) { - SignalStore.releaseChannel.hasUpdatedAvatar = true - setAvatar(recipient.id) + if (recipient.hasServiceId || recipient.hasE164 || recipient.isGroup || recipient.isDistributionList || recipient.isCallLink) { + Log.w(TAG, "Release channel recipient $existingId is not a valid release channel recipient (hasServiceId: ${recipient.hasServiceId}, hasE164: ${recipient.hasE164}, isGroup: ${recipient.isGroup}, isDistributionList: ${recipient.isDistributionList}, isCallLink: ${recipient.isCallLink}). Clearing and recreating.") + SignalStore.releaseChannel.clearReleaseChannelRecipientId() + } else { + Log.i(TAG, "Already created Release Channel recipient $existingId") + if (recipient.profileAvatar.isNullOrEmpty() || !SignalStore.releaseChannel.hasUpdatedAvatar) { + SignalStore.releaseChannel.hasUpdatedAvatar = true + setAvatar(recipient.id) + } + return } - } else { - val recipients = SignalDatabase.recipients - - val releaseChannelId: RecipientId = recipients.insertReleaseChannelRecipient() - SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelId) - SignalStore.releaseChannel.hasUpdatedAvatar = true - - recipients.setProfileName(releaseChannelId, ProfileName.asGiven("Signal")) - recipients.setMuted(releaseChannelId, Long.MAX_VALUE) - setAvatar(releaseChannelId) } + + val recipients = SignalDatabase.recipients + + val releaseChannelId: RecipientId = recipients.insertReleaseChannelRecipient() + SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelId) + SignalStore.releaseChannel.hasUpdatedAvatar = true + + recipients.setProfileName(releaseChannelId, ProfileName.asGiven("Signal")) + recipients.setMuted(releaseChannelId, Long.MAX_VALUE) + setAvatar(releaseChannelId) } private fun setAvatar(id: RecipientId) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 120bfaf59d..6d35f18100 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -92,6 +92,7 @@ import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob; import org.thoughtcrime.securesms.migrations.QuoteThumbnailBackfillMigrationJob; import org.thoughtcrime.securesms.migrations.RebuildMessageSearchIndexMigrationJob; import org.thoughtcrime.securesms.migrations.RecheckPaymentsMigrationJob; +import org.thoughtcrime.securesms.migrations.ReleaseChannelRecipientFixMigrationJob; import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob; import org.thoughtcrime.securesms.migrations.ResetArchiveTierMigrationJob; import org.thoughtcrime.securesms.migrations.SelfRegisteredStateMigrationJob; @@ -344,6 +345,7 @@ public final class JobManagerFactories { put(QuoteThumbnailBackfillMigrationJob.KEY, new QuoteThumbnailBackfillMigrationJob.Factory()); put(RebuildMessageSearchIndexMigrationJob.KEY, new RebuildMessageSearchIndexMigrationJob.Factory()); put(RecheckPaymentsMigrationJob.KEY, new RecheckPaymentsMigrationJob.Factory()); + put(ReleaseChannelRecipientFixMigrationJob.KEY, new ReleaseChannelRecipientFixMigrationJob.Factory()); put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory()); put(ResetArchiveTierMigrationJob.KEY, new ResetArchiveTierMigrationJob.Factory()); put(SelfRegisteredStateMigrationJob.KEY, new SelfRegisteredStateMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ReleaseChannelValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ReleaseChannelValues.kt index 245654e785..6db0b72ed8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ReleaseChannelValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/ReleaseChannelValues.kt @@ -33,6 +33,10 @@ class ReleaseChannelValues(store: KeyValueStore) : SignalStoreValues(store) { putString(KEY_RELEASE_CHANNEL_RECIPIENT_ID, id.serialize()) } + fun clearReleaseChannelRecipientId() { + store.beginWrite().remove(KEY_RELEASE_CHANNEL_RECIPIENT_ID).apply() + } + var nextScheduledCheck by longValue(KEY_NEXT_SCHEDULED_CHECK, 0) var previousManifestMd5 by blobValue(KEY_PREVIOUS_MANIFEST_MD5, ByteArray(0)) var highestVersionNoteReceived by integerValue(KEY_HIGHEST_VERSION_NOTE_RECEIVED, 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 5056872a40..23d5733fb3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -194,9 +194,10 @@ public class ApplicationMigrations { static final int SVR2_ENCLAVE_UPDATE_5 = 150; static final int STICKER_PACK_ADDITION_2 = 151; static final int DELETED_BY_DB_MIGRATION = 152; + static final int RELEASE_CHANNEL_RECIPIENT_FIX = 153; } - public static final int CURRENT_VERSION = 152; + public static final int CURRENT_VERSION = 153; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -899,6 +900,10 @@ public class ApplicationMigrations { jobs.put(Version.DELETED_BY_DB_MIGRATION, new DatabaseMigrationJob()); } + if (lastSeenVersion < Version.RELEASE_CHANNEL_RECIPIENT_FIX) { + jobs.put(Version.RELEASE_CHANNEL_RECIPIENT_FIX, new ReleaseChannelRecipientFixMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ReleaseChannelRecipientFixMigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/ReleaseChannelRecipientFixMigrationJob.kt new file mode 100644 index 0000000000..5d18697625 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ReleaseChannelRecipientFixMigrationJob.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.migrations + +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob + +/** + * In a failed backup flow, the release channel recipient can be incorrectly set. Fix it if that's the case. + */ +internal class ReleaseChannelRecipientFixMigrationJob private constructor(parameters: Parameters) : MigrationJob(parameters) { + + companion object { + const val KEY = "ReleaseChannelRecipientFixMigrationJob" + } + + constructor() : this(Parameters.Builder().build()) + + override fun isUiBlocking(): Boolean = false + + override fun getFactoryKey(): String = KEY + + override fun performMigration() { + AppDependencies.jobManager.add(CreateReleaseChannelJob.create()) + } + + override fun shouldRetry(e: Exception): Boolean = false + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): ReleaseChannelRecipientFixMigrationJob { + return ReleaseChannelRecipientFixMigrationJob(parameters) + } + } +}