From 107ee5268eac62a144381f595dc065332275e13b Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 29 Oct 2025 16:11:44 -0400 Subject: [PATCH] Add some resiliance to custom chat color export. --- .../thoughtcrime/securesms/backup/v2/BackupRepository.kt | 3 ++- .../backup/v2/database/ThreadTableArchiveExtensions.kt | 5 +++-- .../securesms/backup/v2/exporters/ChatArchiveExporter.kt | 8 +++++--- .../backup/v2/processor/AccountDataArchiveProcessor.kt | 8 +++++--- .../securesms/backup/v2/processor/ChatArchiveProcessor.kt | 2 +- .../backup/v2/util/ArchiveConverterExtensions.kt | 6 ++++++ 6 files changed, 22 insertions(+), 10 deletions(-) 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 2b1314eae6..9b55730d54 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 @@ -981,7 +981,7 @@ object BackupRepository { // We're using a snapshot, so the transaction is more for perf than correctness dbSnapshot.rawWritableDatabase.withinTransaction { progressEmitter?.onAccount() - AccountDataArchiveProcessor.export(dbSnapshot, signalStoreSnapshot) { frame -> + AccountDataArchiveProcessor.export(dbSnapshot, signalStoreSnapshot, exportState) { frame -> writer.write(frame) extraFrameOperation?.invoke(frame) eventTimer.emit("account") @@ -2429,6 +2429,7 @@ class ExportState( val threadIdToRecipientId: MutableMap = hashMapOf() val recipientIdToAci: MutableMap = hashMapOf() val aciToRecipientId: MutableMap = hashMapOf() + val customChatColorIds: MutableSet = hashSetOf() } class ImportState(val mediaRootBackupKey: MediaRootBackupKey) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableArchiveExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableArchiveExtensions.kt index 5c24b3a1b4..e901fa5ef1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableArchiveExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableArchiveExtensions.kt @@ -10,13 +10,14 @@ import org.signal.core.util.forEach import org.signal.core.util.requireInt import org.signal.core.util.requireLong import org.signal.core.util.select +import org.thoughtcrime.securesms.backup.v2.ExportState import org.thoughtcrime.securesms.backup.v2.exporters.ChatArchiveExporter import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.ThreadTable -fun ThreadTable.getThreadsForBackup(db: SignalDatabase, includeImageWallpapers: Boolean): ChatArchiveExporter { +fun ThreadTable.getThreadsForBackup(db: SignalDatabase, exportState: ExportState, includeImageWallpapers: Boolean): ChatArchiveExporter { //language=sql val query = """ SELECT @@ -39,7 +40,7 @@ fun ThreadTable.getThreadsForBackup(db: SignalDatabase, includeImageWallpapers: """ val cursor = readableDatabase.query(query) - return ChatArchiveExporter(cursor, db, includeImageWallpapers) + return ChatArchiveExporter(cursor, db, exportState, includeImageWallpapers) } fun ThreadTable.getThreadGroupStatus(messageIds: Collection): Map { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatArchiveExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatArchiveExporter.kt index 10961600a5..36eae334ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatArchiveExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatArchiveExporter.kt @@ -12,8 +12,10 @@ import org.signal.core.util.requireBoolean import org.signal.core.util.requireInt import org.signal.core.util.requireIntOrNull import org.signal.core.util.requireLong +import org.thoughtcrime.securesms.backup.v2.ExportState import org.thoughtcrime.securesms.backup.v2.proto.Chat import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter +import org.thoughtcrime.securesms.backup.v2.util.isValid import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.SignalDatabase @@ -23,7 +25,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper import java.io.Closeable import kotlin.time.Duration.Companion.seconds -class ChatArchiveExporter(private val cursor: Cursor, private val db: SignalDatabase, private val includeImageWallpapers: Boolean) : Iterator, Closeable { +class ChatArchiveExporter(private val cursor: Cursor, private val db: SignalDatabase, private val exportState: ExportState, private val includeImageWallpapers: Boolean) : Iterator, Closeable { override fun hasNext(): Boolean { return cursor.count > 0 && !cursor.isLast } @@ -33,9 +35,9 @@ class ChatArchiveExporter(private val cursor: Cursor, private val db: SignalData throw NoSuchElementException() } - val customChatColorsId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID)) + val customChatColorsId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID)).takeIf { it.isValid(exportState) } ?: ChatColors.Id.NotSet - val chatColors: ChatColors? = cursor.requireBlob(RecipientTable.CHAT_COLORS)?.let { serializedChatColors -> + val chatColors: ChatColors? = cursor.requireBlob(RecipientTable.CHAT_COLORS)?.takeUnless { customChatColorsId is ChatColors.Id.NotSet }?.let { serializedChatColors -> val chatColor = ChatColor.ADAPTER.decodeOrNull(serializedChatColors) chatColor?.let { ChatColors.forChatColor(customChatColorsId, it) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataArchiveProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataArchiveProcessor.kt index f572bd497e..9635d52b2b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataArchiveProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataArchiveProcessor.kt @@ -12,6 +12,7 @@ import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.logging.Log import org.signal.libsignal.zkgroup.backups.BackupLevel import org.thoughtcrime.securesms.attachments.AttachmentId +import org.thoughtcrime.securesms.backup.v2.ExportState import org.thoughtcrime.securesms.backup.v2.ImportState import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.backup.v2.database.restoreSelfFromBackup @@ -21,6 +22,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter +import org.thoughtcrime.securesms.backup.v2.util.isValid import org.thoughtcrime.securesms.backup.v2.util.parseChatWallpaper import org.thoughtcrime.securesms.backup.v2.util.toLocal import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment @@ -54,7 +56,7 @@ object AccountDataArchiveProcessor { private val TAG = Log.tag(AccountDataArchiveProcessor::class) - fun export(db: SignalDatabase, signalStore: SignalStore, emitter: BackupFrameEmitter) { + fun export(db: SignalDatabase, signalStore: SignalStore, exportState: ExportState, emitter: BackupFrameEmitter) { val context = AppDependencies.application val selfId = db.recipientTable.getByAci(signalStore.accountValues.aci!!).get() @@ -104,13 +106,13 @@ object AccountDataArchiveProcessor { displayBadgesOnProfile = signalStore.inAppPaymentValues.getDisplayBadgesOnProfile(), hasSeenGroupStoryEducationSheet = signalStore.storyValues.userHasSeenGroupStoryEducationSheet, hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding(), - customChatColors = db.chatColorsTable.getSavedChatColors().toRemoteChatColors(), + customChatColors = db.chatColorsTable.getSavedChatColors().toRemoteChatColors().also { colors -> exportState.customChatColorIds.addAll(colors.map { it.id }) }, optimizeOnDeviceStorage = signalStore.backupValues.optimizeStorage, backupTier = signalStore.backupValues.backupTier.toRemoteBackupTier(), defaultChatStyle = ChatStyleConverter.constructRemoteChatStyle( db = db, chatColors = chatColors, - chatColorId = chatColors?.id ?: ChatColors.Id.NotSet, + chatColorId = chatColors?.id?.takeIf { it.isValid(exportState) } ?: ChatColors.Id.NotSet, chatWallpaper = chatWallpaper ) ), diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatArchiveProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatArchiveProcessor.kt index aa137d1f93..9accda5a70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatArchiveProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatArchiveProcessor.kt @@ -24,7 +24,7 @@ object ChatArchiveProcessor { val TAG = Log.tag(ChatArchiveProcessor::class.java) fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) { - db.threadTable.getThreadsForBackup(db, includeImageWallpapers = true).use { reader -> + db.threadTable.getThreadsForBackup(db, exportState, includeImageWallpapers = true).use { reader -> for (chat in reader) { if (exportState.recipientIds.contains(chat.recipientId)) { exportState.threadIds.add(chat.id) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveConverterExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveConverterExtensions.kt index 135ebf13a3..2b83286fe7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveConverterExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveConverterExtensions.kt @@ -17,8 +17,10 @@ import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.DatabaseAttachment import org.thoughtcrime.securesms.attachments.PointerAttachment import org.thoughtcrime.securesms.attachments.TombstoneAttachment +import org.thoughtcrime.securesms.backup.v2.ExportState import org.thoughtcrime.securesms.backup.v2.proto.FilePointer import org.thoughtcrime.securesms.conversation.colors.AvatarColor +import org.thoughtcrime.securesms.conversation.colors.ChatColors import org.thoughtcrime.securesms.database.AttachmentTable import org.thoughtcrime.securesms.stickers.StickerLocator import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer @@ -222,6 +224,10 @@ fun RemoteAvatarColor.toLocal(): AvatarColor { } } +fun ChatColors.Id.isValid(exportState: ExportState): Boolean { + return this !is ChatColors.Id.Custom || this.longValue in exportState.customChatColorIds +} + private fun DatabaseAttachment.toRemoteAttachmentType(): AttachmentType { if (this.remoteKey.isNullOrBlank()) { return AttachmentType.INVALID