diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_05.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_05.binproto index e1cd798eaa..4199f41a8d 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_05.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_05.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_06.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_06.binproto index c1c8b8d551..68654feb5c 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_06.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_06.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_07.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_07.binproto index 629a0edd2b..2df0e83a9a 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_07.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_07.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_08.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_08.binproto index e89c066472..a28639a41e 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_08.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_08.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_09.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_09.binproto index d4768b5047..544296ee47 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_09.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_09.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_10.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_10.binproto index 5a310a7986..a97b1f6c57 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_10.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_10.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_11.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_11.binproto index 4775fa305f..445e694ff4 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_11.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_11.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_12.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_12.binproto index c2073d0d21..e4d64cc13c 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_12.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_12.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_13.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_13.binproto index 60a7bc5413..83562eae90 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_13.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_13.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_14.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_14.binproto index 7a5c84b23f..2817d7b29b 100644 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_14.binproto and b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_14.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_15.binproto b/app/src/androidTest/assets/backupTests/chat_item_simple_updates_15.binproto deleted file mode 100644 index aa2f102aa0..0000000000 Binary files a/app/src/androidTest/assets/backupTests/chat_item_simple_updates_15.binproto and /dev/null differ diff --git a/app/src/androidTest/assets/backupTests/chat_release_notes_00.binproto b/app/src/androidTest/assets/backupTests/chat_release_notes_00.binproto deleted file mode 100644 index 87ba9a9cfb..0000000000 Binary files a/app/src/androidTest/assets/backupTests/chat_release_notes_00.binproto and /dev/null differ diff --git a/app/src/androidTest/assets/backupTests/chat_release_notes_01.binproto b/app/src/androidTest/assets/backupTests/chat_release_notes_01.binproto deleted file mode 100644 index b0ced18927..0000000000 Binary files a/app/src/androidTest/assets/backupTests/chat_release_notes_01.binproto and /dev/null differ diff --git a/app/src/androidTest/assets/backupTests/chat_release_notes_02.binproto b/app/src/androidTest/assets/backupTests/chat_release_notes_02.binproto deleted file mode 100644 index 592eaf3638..0000000000 Binary files a/app/src/androidTest/assets/backupTests/chat_release_notes_02.binproto and /dev/null differ diff --git a/app/src/androidTest/assets/backupTests/chat_release_notes_03.binproto b/app/src/androidTest/assets/backupTests/chat_release_notes_03.binproto deleted file mode 100644 index a8d8d59a2b..0000000000 Binary files a/app/src/androidTest/assets/backupTests/chat_release_notes_03.binproto and /dev/null differ diff --git a/app/src/androidTest/assets/backupTests/chat_release_notes_04.binproto b/app/src/androidTest/assets/backupTests/chat_release_notes_04.binproto deleted file mode 100644 index b452c50806..0000000000 Binary files a/app/src/androidTest/assets/backupTests/chat_release_notes_04.binproto and /dev/null differ diff --git a/app/src/androidTest/assets/backupTests/chat_release_notes_05.binproto b/app/src/androidTest/assets/backupTests/chat_release_notes_05.binproto deleted file mode 100644 index 0e5dc43b06..0000000000 Binary files a/app/src/androidTest/assets/backupTests/chat_release_notes_05.binproto and /dev/null differ diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt index e144cb3581..d61388b94a 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt @@ -66,11 +66,6 @@ class ArchiveImportExportTests { runTests { it.matches(Regex("^chat_%d%d.binproto$")) } } -// @Test - fun chatReleaseNotes() { - runTests { it.startsWith("chat_release_notes_") } - } - // @Test fun chatFolders() { runTests { it.startsWith("chat_folder_") } 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 e901fa5ef1..c5f21a3533 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 @@ -5,19 +5,17 @@ package org.thoughtcrime.securesms.backup.v2.database -import org.signal.core.util.SqlUtil -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, exportState: ExportState, includeImageWallpapers: Boolean): ChatArchiveExporter { + val notReleaseNoteClause = exportState.releaseNoteRecipientId?.let { + "AND ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} != $it" + } ?: "" + //language=sql val query = """ SELECT @@ -37,39 +35,9 @@ fun ThreadTable.getThreadsForBackup(db: SignalDatabase, exportState: ExportState LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} WHERE ${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE} NOT IN (${RecipientTable.RecipientType.DISTRIBUTION_LIST.id}, ${RecipientTable.RecipientType.CALL_LINK.id}) + $notReleaseNoteClause """ val cursor = readableDatabase.query(query) return ChatArchiveExporter(cursor, db, exportState, includeImageWallpapers) } - -fun ThreadTable.getThreadGroupStatus(messageIds: Collection): Map { - if (messageIds.isEmpty()) { - return emptyMap() - } - - val out: MutableMap = mutableMapOf() - - val query = SqlUtil.buildFastCollectionQuery("${MessageTable.TABLE_NAME}.${MessageTable.ID}", messageIds) - readableDatabase - .select( - "${MessageTable.TABLE_NAME}.${MessageTable.ID}", - "${RecipientTable.TABLE_NAME}.${RecipientTable.TYPE}" - ) - .from( - """ - ${MessageTable.TABLE_NAME} - INNER JOIN ${ThreadTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID} - INNER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} - """ - ) - .where(query.where, query.whereArgs) - .run() - .forEach { cursor -> - val messageId = cursor.requireLong(MessageTable.ID) - val type = cursor.requireInt(RecipientTable.TYPE) - out[messageId] = type != RecipientTable.RecipientType.INDIVIDUAL.id - } - - return out -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt index b8cafc25cf..fc1d0a827e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt @@ -182,7 +182,7 @@ class ChatItemArchiveExporter( val builder = record.toBasicChatItemBuilder(selfRecipientId, extraData.groupReceiptsById[id], exportState, backupStartTime) transformTimer.emit("basic") - if (builder == null) { + if (builder == null || builder.authorId == exportState.releaseNoteRecipientId) { continue } @@ -1639,7 +1639,7 @@ private fun ChatItem.validateChatItem(exportState: ExportState, selfRecipientId: return null } - if (this.incoming != null && this.authorId != exportState.releaseNoteRecipientId && exportState.recipientIdToAci[this.authorId] == null && exportState.recipientIdToE164[this.authorId] == null) { + if (this.incoming != null && exportState.recipientIdToAci[this.authorId] == null && exportState.recipientIdToE164[this.authorId] == null) { Log.w(TAG, ExportSkips.incomingMessageAuthorDoesNotHaveAciOrE164(this.dateSent)) return null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt index 8bc36d7902..e1103321c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -3168,6 +3168,10 @@ class AttachmentTable( } private fun buildAttachmentsThatNeedUploadQuery(transferStateFilter: String = "$ARCHIVE_TRANSFER_STATE IN (${ArchiveTransferState.NONE.value}, ${ArchiveTransferState.TEMPORARY_FAILURE.value})"): String { + val notReleaseChannelClause = SignalStore.releaseChannel.releaseChannelRecipientId?.let { + "(${MessageTable.TABLE_NAME}.${MessageTable.FROM_RECIPIENT_ID} != ${it.toLong()}) AND" + } ?: "" + return """ $transferStateFilter AND $DATA_FILE NOT NULL AND @@ -3176,6 +3180,7 @@ class AttachmentTable( $TRANSFER_STATE = $TRANSFER_PROGRESS_DONE AND (${MessageTable.STORY_TYPE} = 0 OR ${MessageTable.STORY_TYPE} IS NULL) AND (${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 OR ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} > ${ChatItemArchiveExporter.EXPIRATION_CUTOFF.inWholeMilliseconds}) AND + $notReleaseChannelClause $CONTENT_TYPE != '${MediaUtil.LONG_TEXT}' AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} = 0 """ diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index 6a579073bb..f1f00edcc0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -151,6 +151,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V294_RemoveLastReso import org.thoughtcrime.securesms.database.helpers.migration.V295_AddLastRestoreKeyTypeTableIfMissingMigration import org.thoughtcrime.securesms.database.helpers.migration.V296_RemovePollVoteConstraint import org.thoughtcrime.securesms.database.helpers.migration.V297_AddPinnedMessageColumns +import org.thoughtcrime.securesms.database.helpers.migration.V298_DoNotBackupReleaseNotes import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase /** @@ -308,10 +309,11 @@ object SignalDatabaseMigrations { 294 to V294_RemoveLastResortKeyTupleColumnConstraintMigration, 295 to V295_AddLastRestoreKeyTypeTableIfMissingMigration, 296 to V296_RemovePollVoteConstraint, - 297 to V297_AddPinnedMessageColumns + 297 to V297_AddPinnedMessageColumns, + 298 to V298_DoNotBackupReleaseNotes ) - const val DATABASE_VERSION = 297 + const val DATABASE_VERSION = 298 @JvmStatic fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V298_DoNotBackupReleaseNotes.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V298_DoNotBackupReleaseNotes.kt new file mode 100644 index 0000000000..f33213c219 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V298_DoNotBackupReleaseNotes.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import org.signal.core.util.SqlUtil +import org.signal.core.util.logging.Log +import org.signal.core.util.requireLong +import org.thoughtcrime.securesms.database.KeyValueDatabase +import org.thoughtcrime.securesms.database.SQLiteDatabase + +object V298_DoNotBackupReleaseNotes : SignalDatabaseMigration { + + private val TAG = Log.tag(V298_DoNotBackupReleaseNotes::class.java) + + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + val releaseNoteRecipientId = getReleaseNoteRecipientId(context) ?: return + migrateWithRecipientId(db, releaseNoteRecipientId) + } + + fun migrateWithRecipientId(db: SQLiteDatabase, releaseNoteRecipientId: Long) { + db.execSQL( + """ + UPDATE attachment + SET archive_transfer_state = 0 + WHERE message_id IN ( + SELECT _id FROM message WHERE from_recipient_id = $releaseNoteRecipientId + ) + """ + ) + } + + private fun getReleaseNoteRecipientId(context: Application): Long? { + return if (KeyValueDatabase.exists(context)) { + val keyValueDatabase = KeyValueDatabase.getInstance(context).readableDatabase + keyValueDatabase.query("key_value", arrayOf("value"), "key = ?", SqlUtil.buildArgs("releasechannel.recipient_id"), null, null, null).use { cursor -> + if (cursor.moveToFirst()) { + cursor.requireLong("value") + } else { + Log.w(TAG, "Release note channel recipient ID not found in KV database!") + null + } + } + } else { + Log.w(TAG, "Pre-KV database, not doing anything.") + null + } + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/helpers/migration/V298_DoNotBackupReleaseNotesTest.kt b/app/src/test/java/org/thoughtcrime/securesms/database/helpers/migration/V298_DoNotBackupReleaseNotesTest.kt new file mode 100644 index 0000000000..08ed410908 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/database/helpers/migration/V298_DoNotBackupReleaseNotesTest.kt @@ -0,0 +1,198 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import android.content.ContentValues +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.testutil.SignalDatabaseMigrationRule + +@Suppress("ClassName") +@RunWith(RobolectricTestRunner::class) +@Config(manifest = Config.NONE, application = Application::class) +class V298_DoNotBackupReleaseNotesTest { + + @get:Rule val signalDatabaseRule = SignalDatabaseMigrationRule(297) + + private val releaseNoteRecipientId = 100L + private val otherRecipientId = 200L + + @Test + fun `migrate - attachments from release note recipient - clears archive transfer state`() { + val messageId = insertMessage(fromRecipientId = releaseNoteRecipientId) + val attachmentId = insertAttachment( + messageId = messageId, + archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value, + archiveCdn = 2 + ) + + runMigration() + + assertArchiveState(attachmentId, expectedState = AttachmentTable.ArchiveTransferState.NONE.value) + } + + @Test + fun `migrate - attachments from other recipient - no change`() { + val messageId = insertMessage(fromRecipientId = otherRecipientId) + val attachmentId = insertAttachment( + messageId = messageId, + archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value, + archiveCdn = 2 + ) + + runMigration() + + assertArchiveState(attachmentId, expectedState = AttachmentTable.ArchiveTransferState.FINISHED.value) + } + + @Test + fun `migrate - multiple attachments from release note recipient - all cleared`() { + val messageId1 = insertMessage(fromRecipientId = releaseNoteRecipientId) + val messageId2 = insertMessage(fromRecipientId = releaseNoteRecipientId) + + val attachmentId1 = insertAttachment( + messageId = messageId1, + archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value, + archiveCdn = 2 + ) + val attachmentId2 = insertAttachment( + messageId = messageId2, + archiveTransferState = AttachmentTable.ArchiveTransferState.UPLOAD_IN_PROGRESS.value, + archiveCdn = 3 + ) + + runMigration() + + assertArchiveState(attachmentId1, expectedState = AttachmentTable.ArchiveTransferState.NONE.value) + assertArchiveState(attachmentId2, expectedState = AttachmentTable.ArchiveTransferState.NONE.value) + } + + @Test + fun `migrate - mixed recipients - only release note attachments cleared`() { + val releaseNoteMessageId = insertMessage(fromRecipientId = releaseNoteRecipientId) + val otherMessageId = insertMessage(fromRecipientId = otherRecipientId) + + val releaseNoteAttachmentId = insertAttachment( + messageId = releaseNoteMessageId, + archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value, + archiveCdn = 2 + ) + val otherAttachmentId = insertAttachment( + messageId = otherMessageId, + archiveTransferState = AttachmentTable.ArchiveTransferState.FINISHED.value, + archiveCdn = 2 + ) + + runMigration() + + assertArchiveState(releaseNoteAttachmentId, expectedState = AttachmentTable.ArchiveTransferState.NONE.value) + assertArchiveState(otherAttachmentId, expectedState = AttachmentTable.ArchiveTransferState.FINISHED.value) + } + + private fun runMigration() { + V298_DoNotBackupReleaseNotes.migrateWithRecipientId( + db = signalDatabaseRule.database, + releaseNoteRecipientId = releaseNoteRecipientId + ) + } + + private val insertedRecipients = mutableSetOf() + private val insertedThreads = mutableMapOf() + private var dateSentCounter = System.currentTimeMillis() + + private fun insertRecipient(recipientId: Long): Long { + if (recipientId in insertedRecipients) { + return recipientId + } + + val db = signalDatabaseRule.database + + val values = ContentValues().apply { + put("_id", recipientId) + put("type", 0) + } + + db.insert("recipient", null, values) + insertedRecipients.add(recipientId) + return recipientId + } + + private fun insertThread(recipientId: Long): Long { + insertedThreads[recipientId]?.let { return it } + + val db = signalDatabaseRule.database + + val values = ContentValues().apply { + put("recipient_id", recipientId) + put("date", System.currentTimeMillis()) + } + + val threadId = db.insert("thread", null, values) + insertedThreads[recipientId] = threadId + return threadId + } + + private fun insertMessage(fromRecipientId: Long): Long { + val db = signalDatabaseRule.database + + insertRecipient(fromRecipientId) + val threadId = insertThread(fromRecipientId) + dateSentCounter++ + + val values = ContentValues().apply { + put("date_sent", dateSentCounter) + put("date_received", System.currentTimeMillis()) + put("thread_id", threadId) + put("from_recipient_id", fromRecipientId) + put("to_recipient_id", fromRecipientId) + put("type", 0) + } + + return db.insert("message", null, values) + } + + private fun insertAttachment(messageId: Long, archiveTransferState: Int, archiveCdn: Int?): Long { + val db = signalDatabaseRule.database + + val values = ContentValues().apply { + put("message_id", messageId) + put("data_file", "/fake/path/attachment.jpg") + put("data_random", "/fake/path/attachment.jpg".toByteArray()) + put("transfer_state", 0) + put("archive_transfer_state", archiveTransferState) + if (archiveCdn != null) { + put("archive_cdn", archiveCdn) + } + } + + return db.insert("attachment", null, values) + } + + private fun assertArchiveState(attachmentId: Long, expectedState: Int) { + val db = signalDatabaseRule.database + val cursor = db.query( + "attachment", + arrayOf("archive_transfer_state"), + "_id = ?", + arrayOf(attachmentId.toString()), + null, + null, + null + ) + + cursor.use { + assertThat(it.moveToFirst()).isEqualTo(true) + assertThat(it.getInt(it.getColumnIndexOrThrow("archive_transfer_state"))).isEqualTo(expectedState) + } + } +}