From 5ffb7b07da8615f73132b046c3d67b0393cab5d2 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Fri, 9 Aug 2024 16:04:47 -0400 Subject: [PATCH] Update to latest Backup.proto and fix various backup bugs. --- .../backup/v2/ArchiveImportExportTests.kt | 291 +++ .../securesms/backup/v2/ImportExportTest.kt | 1750 ----------------- .../backup/v2/ImportExportTestSuite.kt | 118 -- .../RecipientTableTest_getAndPossiblyMerge.kt | 4 +- .../attachments/ArchivedAttachment.kt | 5 +- .../thoughtcrime/securesms/attachments/Cdn.kt | 2 +- .../attachments/DatabaseAttachment.kt | 2 +- .../attachments/TombstoneAttachment.kt | 2 +- .../securesms/attachments/UriAttachment.kt | 4 +- .../securesms/backup/v2/BackupRepository.kt | 8 +- .../ChatColorsTableBackupExtensions.kt | 13 + .../v2/database/ChatItemExportIterator.kt | 174 +- .../v2/database/ChatItemImportInserter.kt | 336 ++-- .../InAppPaymentTableBackupExtensions.kt | 13 + .../database/MessageTableBackupExtensions.kt | 4 +- .../database/ReactionTableBackupExtensions.kt | 13 + .../RecipientTableBackupExtensions.kt | 79 +- .../v2/processor/ChatItemBackupProcessor.kt | 2 +- .../securesms/database/AttachmentTable.kt | 1 + .../securesms/database/ChatColorsTable.kt | 2 +- .../securesms/database/InAppPaymentTable.kt | 2 +- .../securesms/database/KeyValueDatabase.java | 4 + .../model/GroupsV2UpdateMessageConverter.kt | 2 +- .../ApplicationDependencyProvider.java | 4 +- .../jobs/ArchiveThumbnailUploadJob.kt | 4 +- .../securesms/keyvalue/SignalStore.kt | 5 - app/src/main/protowire/Backup.proto | 63 +- .../org/signal/core/util/CursorExtensions.kt | 9 + dependencies.gradle.kts | 2 +- gradle/verification-metadata.xml | 20 +- .../websocket/LibSignalNetworkExtensions.kt | 5 +- 31 files changed, 761 insertions(+), 2182 deletions(-) create mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt delete mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt delete mode 100644 app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTestSuite.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatColorsTableBackupExtensions.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/InAppPaymentTableBackupExtensions.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ReactionTableBackupExtensions.kt 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 new file mode 100644 index 0000000000..1f11057dc4 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt @@ -0,0 +1,291 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2 + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.github.difflib.DiffUtils +import com.github.difflib.UnifiedDiffUtils +import junit.framework.Assert.assertTrue +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.signal.core.util.Base64 +import org.signal.core.util.SqlUtil +import org.signal.core.util.logging.Log +import org.signal.core.util.readFully +import org.signal.libsignal.messagebackup.ComparableBackup +import org.signal.libsignal.messagebackup.MessageBackup +import org.signal.libsignal.zkgroup.profiles.ProfileKey +import org.thoughtcrime.securesms.backup.v2.proto.Frame +import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader +import org.thoughtcrime.securesms.database.DistributionListTables +import org.thoughtcrime.securesms.database.KeyValueDatabase +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.RecipientId +import org.whispersystems.signalservice.api.kbs.MasterKey +import org.whispersystems.signalservice.api.push.ServiceId +import java.io.ByteArrayInputStream +import java.util.UUID + +@RunWith(AndroidJUnit4::class) +class ArchiveImportExportTests { + + companion object { + const val TAG = "ImportExport" + const val TESTS_FOLDER = "backupTests" + + val SELF_ACI = ServiceId.ACI.from(UUID(100, 100)) + val SELF_PNI = ServiceId.PNI.from(UUID(101, 101)) + val SELF_E164 = "+10000000000" + val SELF_PROFILE_KEY: ByteArray = Base64.decode("YQKRq+3DQklInaOaMcmlzZnN0m/1hzLiaONX7gB12dg=") + val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do") + } + + @Test + fun all() { + runTests() + } + + @Ignore("Just for debugging") + @Test + fun accountData() { + runTests { it.startsWith("account_data_") } + } + + @Ignore("Just for debugging") + @Test + fun recipientContacts() { + runTests { it.startsWith("recipient_contacts_") } + } + + @Ignore("Just for debugging") + @Test + fun recipientDistributionLists() { + runTests { it.startsWith("recipient_distribution_list_") } + } + + @Ignore("Just for debugging") + @Test + fun recipientGroups() { + runTests { it.startsWith("recipient_groups_") } + } + + @Ignore("Just for debugging") + @Test + fun chatStandardMessageTextOnly() { + runTests { it.startsWith("chat_standard_message_text_only_") } + } + + @Ignore("Just for debugging") + @Test + fun chatStandardMessageFormattedText() { + runTests { it.startsWith("chat_standard_message_formatted_text_") } + } + + @Ignore("Just for debugging") + @Test + fun chatStandardMessageLongText() { + runTests { it.startsWith("chat_standard_message_long_text_") } + } + + @Ignore("Just for debugging") + @Test + fun chatStandardMessageStandardAttachments() { + runTests { it.startsWith("chat_standard_message_standard_attachments_") } + } + + @Ignore("Just for debugging") + @Test + fun chatStandardMessageSpecialAttachments() { + runTests { it.startsWith("chat_standard_message_special_attachments_") } + } + + @Ignore("Just for debugging") + @Test + fun chatSimpleUpdates() { + runTests { it.startsWith("chat_simple_updates_") } + } + + @Ignore("Just for debugging") + @Test + fun chatContactMessage() { + runTests { it.startsWith("chat_contact_message_") } + } + + private fun runTests(predicate: (String) -> Boolean = { true }) { + val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)!!.filter(predicate) + val results: MutableList = mutableListOf() + + Log.d(TAG, "About to run ${testFiles.size} tests.") + + for (filename in testFiles) { + Log.d(TAG, "> $filename") + val startTime = System.currentTimeMillis() + val result = test(filename) + results += result + + if (result is TestResult.Success) { + Log.d(TAG, " \uD83D\uDFE2 Passed in ${System.currentTimeMillis() - startTime} ms") + } else { + Log.d(TAG, " \uD83D\uDD34 Failed in ${System.currentTimeMillis() - startTime} ms") + } + } + + results + .filterIsInstance() + .forEach { + Log.e(TAG, "Failure: ${it.name}\n${it.message}") + Log.e(TAG, "----------------------------------") + Log.e(TAG, "----------------------------------") + Log.e(TAG, "----------------------------------") + } + + if (results.any { it is TestResult.Failure }) { + val successCount = results.count { it is TestResult.Success } + val failingTestNames = results.filterIsInstance().joinToString(separator = "\n") { " \uD83D\uDD34 ${it.name}" } + val message = "Some tests failed! Only $successCount/${results.size} passed. Failure details are above. Failing tests:\n$failingTestNames" + + Log.d(TAG, message) + throw AssertionError(message) + } else { + Log.d(TAG, "All ${results.size} tests passed!") + } + } + + private fun test(filename: String): TestResult { + resetAllData() + + val inputFileBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("$TESTS_FOLDER/$filename").readFully(true) + + val importResult = import(inputFileBytes) + assertTrue(importResult is ImportResult.Success) + val success = importResult as ImportResult.Success + + val generatedBackupData = BackupRepository.debugExport(plaintext = true, currentTime = success.backupTime) + checkEquivalent(filename, inputFileBytes, generatedBackupData)?.let { return it } + + // Validator expects encrypted data, so we have to export again with encryption to validate + val encryptedBackupData = BackupRepository.debugExport(plaintext = false, currentTime = success.backupTime) + assertPassesValidator(filename, encryptedBackupData)?.let { return it } + + return TestResult.Success(filename) + } + + private fun resetAllData() { + // Need to delete these first to prevent foreign key crash + SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}") + SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}") + + SqlUtil.getAllTables(SignalDatabase.rawDatabase) + .filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB + .sorted() + .forEach { table -> + SignalDatabase.rawDatabase.execSQL("DELETE FROM $table") + SqlUtil.resetAutoIncrementValue(SignalDatabase.rawDatabase, table) + } + + AppDependencies.recipientCache.clear() + AppDependencies.recipientCache.clearSelf() + RecipientId.clearCache() + + KeyValueDatabase.getInstance(AppDependencies.application).clear() + SignalStore.resetCache() + + SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234") + SignalStore.account.setE164(SELF_E164) + SignalStore.account.setAci(SELF_ACI) + SignalStore.account.setPni(SELF_PNI) + SignalStore.account.generateAciIdentityKeyIfNecessary() + SignalStore.account.generatePniIdentityKeyIfNecessary() + SignalStore.backup.backupTier = MessageBackupTier.PAID + } + + private fun import(importData: ByteArray): ImportResult { + return BackupRepository.import( + length = importData.size.toLong(), + inputStreamFactory = { ByteArrayInputStream(importData) }, + selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)), + plaintext = true + ) + } + + private fun assertPassesValidator(testName: String, generatedBackupData: ByteArray): TestResult.Failure? { + try { + BackupRepository.validate( + length = generatedBackupData.size.toLong(), + inputStreamFactory = { ByteArrayInputStream(generatedBackupData) }, + selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)) + ) + } catch (e: Exception) { + return TestResult.Failure(testName, "Generated backup failed validation: ${e.message}") + } + + return null + } + + private fun checkEquivalent(testName: String, import: ByteArray, export: ByteArray): TestResult.Failure? { + val importComparable = try { + ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong()) + } catch (e: Exception) { + return TestResult.Failure(testName, "Imported backup hit a validation error: ${e.message}") + } + + val exportComparable = try { + ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, export.inputStream(), import.size.toLong()) + } catch (e: Exception) { + return TestResult.Failure(testName, "Exported backup hit a validation error: ${e.message}") + } + + if (importComparable.unknownFieldMessages.isNotEmpty()) { + return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}") + } + + if (exportComparable.unknownFieldMessages.isNotEmpty()) { + return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}") + } + + val canonicalImport = importComparable.comparableString + val canonicalExport = exportComparable.comparableString + + if (canonicalImport != canonicalExport) { + val importLines = canonicalImport.lines() + val exportLines = canonicalExport.lines() + + val patch = DiffUtils.diff(importLines, exportLines) + val diff = UnifiedDiffUtils.generateUnifiedDiff("Import", "Export", importLines, patch, 3).joinToString(separator = "\n") + + val importFrames = import.toFrames() + val exportFrames = export.toFrames() + + val importGroupFramesByMasterKey = importFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey } + val exportGroupFramesByMasterKey = exportFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey } + + val groupErrorMessage = StringBuilder() + + for ((importKey, importValue) in importGroupFramesByMasterKey) { + if (exportGroupFramesByMasterKey[importKey]?.let { it.snapshot != importValue.snapshot } == true) { + groupErrorMessage.append("[$importKey] Snapshot mismatch.\nImport:\n${importValue}\n\nExport:\n${exportGroupFramesByMasterKey[importKey]}\n\n") + } + } + + return TestResult.Failure(testName, "Imported backup does not match exported backup. Diff:\n$diff\n$groupErrorMessage") + } + + return null + } + + fun ByteArray.toFrames(): List { + return PlainTextBackupReader(this.inputStream(), this.size.toLong()).use { it.asSequence().toList() } + } + + private sealed class TestResult(val name: String) { + class Success(name: String) : TestResult(name) + class Failure(name: String, val message: String) : TestResult(name) + } +} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt deleted file mode 100644 index 8b0c43a01b..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt +++ /dev/null @@ -1,1750 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.backup.v2 - -import android.Manifest -import android.app.UiAutomation -import android.content.Context -import android.os.Environment -import androidx.test.platform.app.InstrumentationRegistry -import okio.ByteString.Companion.toByteString -import org.junit.Assert -import org.junit.Before -import org.junit.Ignore -import org.junit.Rule -import org.junit.Test -import org.junit.rules.TestName -import org.signal.core.util.Base64 -import org.signal.core.util.test.getObjectDiff -import org.signal.libsignal.messagebackup.MessageBackup -import org.signal.libsignal.messagebackup.MessageBackupKey -import org.signal.libsignal.zkgroup.profiles.ProfileKey -import org.thoughtcrime.securesms.backup.v2.proto.AccountData -import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall -import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo -import org.thoughtcrime.securesms.backup.v2.proto.BodyRange -import org.thoughtcrime.securesms.backup.v2.proto.Chat -import org.thoughtcrime.securesms.backup.v2.proto.ChatItem -import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage -import org.thoughtcrime.securesms.backup.v2.proto.Contact -import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment -import org.thoughtcrime.securesms.backup.v2.proto.ContactMessage -import org.thoughtcrime.securesms.backup.v2.proto.DistributionList -import org.thoughtcrime.securesms.backup.v2.proto.DistributionListItem -import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate -import org.thoughtcrime.securesms.backup.v2.proto.FilePointer -import org.thoughtcrime.securesms.backup.v2.proto.Frame -import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge -import org.thoughtcrime.securesms.backup.v2.proto.Group -import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall -import org.thoughtcrime.securesms.backup.v2.proto.LinkPreview -import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment -import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate -import org.thoughtcrime.securesms.backup.v2.proto.Quote -import org.thoughtcrime.securesms.backup.v2.proto.Reaction -import org.thoughtcrime.securesms.backup.v2.proto.Recipient -import org.thoughtcrime.securesms.backup.v2.proto.ReleaseNotes -import org.thoughtcrime.securesms.backup.v2.proto.Self -import org.thoughtcrime.securesms.backup.v2.proto.SendStatus -import org.thoughtcrime.securesms.backup.v2.proto.SessionSwitchoverChatUpdate -import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate -import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage -import org.thoughtcrime.securesms.backup.v2.proto.StickerPack -import org.thoughtcrime.securesms.backup.v2.proto.Text -import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate -import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter -import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader -import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter -import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.util.Util -import org.whispersystems.signalservice.api.kbs.MasterKey -import org.whispersystems.signalservice.api.push.DistributionId -import org.whispersystems.signalservice.api.push.ServiceId -import org.whispersystems.signalservice.api.subscriptions.SubscriberId -import org.whispersystems.signalservice.api.util.toByteArray -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.FileOutputStream -import java.util.UUID -import java.util.concurrent.TimeUnit -import kotlin.random.Random -import kotlin.time.Duration.Companion.days - -/** - * Test the import and export of message backup frames to make sure what - * goes in, comes out. - */ -@Ignore("Deprecated") -class ImportExportTest { - companion object { - /** - * Output the frames as a plaintext .binproto for sharing tests - * - * This only seems to work on API 28 emulators, You can find the generated files - * at /sdcard/backup-tests/ - * */ - val OUTPUT_FILES = false - - val SELF_ACI = ServiceId.ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641")) - val SELF_PNI = ServiceId.PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910")) - const val SELF_E164 = "+10000000000" - val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32)) - val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do") - - val defaultBackupInfo = BackupInfo(version = 1L, backupTimeMs = 123456L) - val selfRecipient = Recipient(id = 1, self = Self()) - val myStory = Recipient( - id = 2, - distributionList = DistributionListItem( - distributionId = DistributionId.MY_STORY.asUuid().toByteArray().toByteString(), - distributionList = DistributionList( - name = DistributionId.MY_STORY.toString(), - privacyMode = DistributionList.PrivacyMode.ALL - ) - ) - ) - val releaseNotes = Recipient(id = 3, releaseNotes = ReleaseNotes()) - val standardAccountData = AccountData( - profileKey = SELF_PROFILE_KEY.serialize().toByteString(), - username = "self.01", - usernameLink = null, - givenName = "Peter", - familyName = "Parker", - avatarUrlPath = "https://example.com/", - donationSubscriberData = AccountData.SubscriberData( - subscriberId = SubscriberId.generate().bytes.toByteString(), - currencyCode = "USD", - manuallyCancelled = true - ), - accountSettings = AccountData.AccountSettings( - readReceipts = true, - sealedSenderIndicators = true, - typingIndicators = true, - linkPreviews = true, - notDiscoverableByPhoneNumber = true, - preferContactAvatars = true, - universalExpireTimerSeconds = 42, - displayBadgesOnProfile = true, - keepMutedChatsArchived = true, - hasSetMyStoriesPrivacy = true, - hasViewedOnboardingStory = true, - storiesDisabled = true, - storyViewReceiptsEnabled = true, - hasSeenGroupStoryEducationSheet = true, - hasCompletedUsernameOnboarding = true, - phoneNumberSharingMode = AccountData.PhoneNumberSharingMode.EVERYBODY, - preferredReactionEmoji = listOf("a", "b", "c") - ) - ) - val alice = Recipient( - id = 4, - contact = Contact( - aci = TestRecipientUtils.nextAci().toByteString(), - pni = TestRecipientUtils.nextPni().toByteString(), - username = "cool.01", - e164 = 141255501234, - blocked = false, - visibility = Contact.Visibility.VISIBLE, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = true, - profileGivenName = "Alexa", - profileFamilyName = "Kim", - hideStory = true - ) - ) - - /** - * When using standardFrames you must start recipient ids at 4. - */ - private val standardFrames = arrayOf(defaultBackupInfo, standardAccountData, selfRecipient, myStory, releaseNotes) - } - - private val context: Context - get() = InstrumentationRegistry.getInstrumentation().targetContext - - @JvmField - @Rule - var testName = TestName() - - @Before - fun setup() { - SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234") - SignalStore.account.setE164(SELF_E164) - SignalStore.account.setAci(SELF_ACI) - SignalStore.account.setPni(SELF_PNI) - SignalStore.account.generateAciIdentityKeyIfNecessary() - SignalStore.account.generatePniIdentityKeyIfNecessary() - } - - @Test - fun accountAndSelf() { - importExport(*standardFrames) - } - - @Test - fun largeNumberOfRecipientsAndChats() { - val recipients = ArrayList(5000) - val chats = ArrayList(5000) - var id = 4L - for (i in 0..5000) { - val recipientId = id++ - recipients.add( - Recipient( - id = recipientId, - contact = Contact( - aci = TestRecipientUtils.nextAci().toByteString(), - pni = TestRecipientUtils.nextPni().toByteString(), - username = "rec$i.01", - e164 = 14125550000 + i, - blocked = false, - visibility = Contact.Visibility.VISIBLE, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = true, - profileGivenName = "Test", - profileFamilyName = "Recipient$i", - hideStory = false - ) - ) - ) - chats.add( - Chat( - id = recipientId - 2L, - recipientId = recipientId - ) - ) - if (i % 10 == 0) { - val groupId = id++ - recipients.add( - Recipient( - id = groupId, - group = Group( - masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), - whitelisted = true, - hideStory = false, - storySendMode = Group.StorySendMode.ENABLED - ) - ) - ) - chats.add( - Chat( - id = groupId - 2L, - recipientId = groupId - ) - ) - } - } - importExport( - *standardFrames, - *recipients.toArray() - ) - } - - @Test - fun largeNumberOfMessagesAndChats() { - val numIndividualRecipients = 1000 - val numIndividualMessages = 500 - val numGroupMessagesPerPerson = 200 - - val random = Random(1516) - - val recipients = ArrayList(1010) - val chats = ArrayList(1010) - var id = 3L - for (i in 0 until numIndividualRecipients) { - val recipientId = id++ - recipients.add( - Recipient( - id = recipientId, - contact = Contact( - aci = TestRecipientUtils.nextAci().toByteString(), - pni = TestRecipientUtils.nextPni().toByteString(), - username = if (random.trueWithProbability(0.2f)) "rec$i.01" else null, - e164 = 14125550000 + i, - blocked = random.trueWithProbability(0.1f), - visibility = if (random.trueWithProbability(0.1f)) Contact.Visibility.HIDDEN else Contact.Visibility.VISIBLE, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = random.trueWithProbability(0.9f), - profileGivenName = "Test", - profileFamilyName = "Recipient$i", - hideStory = false - ) - ) - ) - chats.add( - Chat( - id = recipientId - 2L, - recipientId = recipientId - ) - ) - if (i % 100 == 0) { - val groupId = id++ - recipients.add( - Recipient( - id = groupId, - group = Group( - masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), - whitelisted = random.trueWithProbability(0.9f), - hideStory = random.trueWithProbability(0.1f), - storySendMode = if (random.trueWithProbability(0.9f)) Group.StorySendMode.ENABLED else Group.StorySendMode.DISABLED - ) - ) - ) - chats.add( - Chat( - id = groupId - 2L, - recipientId = groupId - ) - ) - } - } - val chatItems = ArrayList() - var sentTime = 1L - val groupMembers = ArrayList() - var group: Recipient? = null - for (recipient in recipients) { - // Make another group and populate it with messages from these members - if (recipient.group != null) { - if (group == null) { - group = recipient - groupMembers.clear() - } else { - for (member in groupMembers) { - for (i in 0 until numGroupMessagesPerPerson) { - chatItems.add( - ChatItem( - chatId = group.id - 2L, - authorId = member.id, - dateSent = sentTime++, - sms = false, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = sentTime + 1, - dateServerSent = sentTime, - read = true, - sealedSender = true - ), - standardMessage = StandardMessage( - text = Text( - body = "Medium length message from ${member.contact?.profileGivenName} ${member.contact?.profileFamilyName} sent at $sentTime" - ) - ) - ) - ) - } - } - for (i in 0 until numGroupMessagesPerPerson) { - ChatItem( - chatId = group.id - 2L, - authorId = selfRecipient.id, - dateSent = sentTime++, - sms = false, - outgoing = ChatItem.OutgoingMessageDetails( - sendStatus = groupMembers.map { groupMember -> - SendStatus(recipientId = groupMember.id, deliveryStatus = if (random.trueWithProbability(0.8f)) SendStatus.Status.READ else SendStatus.Status.DELIVERED, sealedSender = true) - } - ), - standardMessage = StandardMessage( - text = Text( - body = "Outgoing message without much text in it just because" - ) - ) - ) - } - } - } else { - groupMembers.add(recipient) - for (i in 0 until numIndividualMessages) { - if (i % 2 == 0) { - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = sentTime++, - sms = false, - outgoing = ChatItem.OutgoingMessageDetails( - sendStatus = listOf( - SendStatus(recipient.id, deliveryStatus = if (random.trueWithProbability(0.8f)) SendStatus.Status.READ else SendStatus.Status.DELIVERED, sealedSender = true) - ) - ), - standardMessage = StandardMessage( - text = Text( - body = "Outgoing message without much text in it just because" - ) - ) - ) - } else { - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = sentTime++, - sms = false, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = sentTime + 1, - dateServerSent = sentTime, - read = true, - sealedSender = true - ), - standardMessage = StandardMessage( - text = Text( - body = "Outgoing message without much text in it just because" - ) - ) - ) - } - } - } - } - - exportFrames( - *standardFrames, - *recipients.toArray(), - *chatItems.toArray() - ) - } - - @Test - fun individualRecipients() { - importExport( - *standardFrames, - Recipient( - id = 4, - contact = Contact( - aci = TestRecipientUtils.nextAci().toByteString(), - pni = TestRecipientUtils.nextPni().toByteString(), - username = "cool.01", - e164 = 141255501234, - blocked = true, - visibility = Contact.Visibility.VISIBLE, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = true, - profileGivenName = "Alexa", - profileFamilyName = "Kim", - hideStory = true - ) - ), - Recipient( - id = 5, - contact = Contact( - aci = null, - pni = null, - username = null, - e164 = 141255501235, - blocked = true, - visibility = Contact.Visibility.HIDDEN, - notRegistered = Contact.NotRegistered(unregisteredTimestamp = 1234568927398L), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = false, - profileGivenName = "Peter", - profileFamilyName = "Kim", - hideStory = true - ) - ) - ) - } - - @Test - fun groupRecipients() { - importExport( - *standardFrames, - Recipient( - id = 4, - group = Group( - masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), - whitelisted = true, - hideStory = true, - storySendMode = Group.StorySendMode.ENABLED, - snapshot = Group.GroupSnapshot( - title = Group.GroupAttributeBlob(title = "Group Cool"), - description = Group.GroupAttributeBlob(descriptionText = "Description"), - version = 10, - disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = 1500000) - ) - ) - ), - Recipient( - id = 5, - group = Group( - masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), - whitelisted = false, - hideStory = false, - storySendMode = Group.StorySendMode.DEFAULT, - snapshot = Group.GroupSnapshot( - title = Group.GroupAttributeBlob(title = "Group Cool"), - description = Group.GroupAttributeBlob(descriptionText = "Description"), - version = 10, - disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = 1500000) - ) - ) - ) - ) - } - - @Test - fun distributionListRecipients() { - importExport( - *standardFrames, - Recipient( - id = 4, - contact = Contact( - aci = TestRecipientUtils.nextAci().toByteString(), - pni = TestRecipientUtils.nextPni().toByteString(), - username = "cool.01", - e164 = 141255501234, - blocked = true, - visibility = Contact.Visibility.HIDDEN, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = true, - profileGivenName = "Alexa", - profileFamilyName = "Kim", - hideStory = true - ) - ), - Recipient( - id = 5, - contact = Contact( - aci = null, - pni = null, - username = null, - e164 = 141255501235, - blocked = true, - visibility = Contact.Visibility.HIDDEN, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = true, - profileGivenName = "Peter", - profileFamilyName = "Kim", - hideStory = true - ) - ), - Recipient( - id = 6, - contact = Contact( - aci = null, - pni = null, - username = null, - e164 = 141255501236, - blocked = true, - visibility = Contact.Visibility.HIDDEN, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = true, - profileGivenName = "Father", - profileFamilyName = "Kim", - hideStory = true - ) - ), - Recipient( - id = 7, - distributionList = DistributionListItem( - distributionId = DistributionId.create().asUuid().toByteArray().toByteString(), - distributionList = DistributionList( - name = "Kim Family", - allowReplies = true, - privacyMode = DistributionList.PrivacyMode.ONLY_WITH, - memberRecipientIds = listOf(3, 4, 5) - ) - ) - ) - ) - } - - @Test - fun chatThreads() { - importExport( - *standardFrames, - Recipient( - id = 4, - contact = Contact( - aci = TestRecipientUtils.nextAci().toByteString(), - pni = TestRecipientUtils.nextPni().toByteString(), - username = "cool.01", - e164 = 141255501234, - blocked = false, - visibility = Contact.Visibility.VISIBLE, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = true, - profileGivenName = "Alexa", - profileFamilyName = "Kim", - hideStory = true - ) - ), - Recipient( - id = 5, - group = Group( - masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), - whitelisted = true, - hideStory = true, - storySendMode = Group.StorySendMode.DEFAULT - ) - ), - Chat( - id = 1, - recipientId = 4, - archived = true, - pinnedOrder = 1, - expirationTimerMs = 1.days.inWholeMilliseconds, - muteUntilMs = System.currentTimeMillis(), - markedUnread = true, - dontNotifyForMentionsIfMuted = true - ) - ) - } - - @Test - fun individualCalls() { - val individualCalls = ArrayList() - val states = arrayOf(IndividualCall.State.ACCEPTED, IndividualCall.State.NOT_ACCEPTED, IndividualCall.State.MISSED, IndividualCall.State.MISSED_NOTIFICATION_PROFILE) - val oldStates = arrayOf(IndividualCall.State.ACCEPTED, IndividualCall.State.MISSED) - val types = arrayOf(IndividualCall.Type.VIDEO_CALL, IndividualCall.Type.AUDIO_CALL) - val directions = arrayOf(IndividualCall.Direction.OUTGOING, IndividualCall.Direction.INCOMING) - var sentTime = 0L - var callId = 1L - val startedAci = TestRecipientUtils.nextAci().toByteString() - for (state in states) { - for (type in types) { - for (direction in directions) { - // With call id - individualCalls.add( - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = sentTime++, - sms = false, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - individualCall = IndividualCall( - callId = callId++, - type = type, - state = state, - direction = direction - ) - ) - ) - ) - } - } - } - for (state in oldStates) { - for (type in types) { - for (direction in directions) { - if (state == IndividualCall.State.MISSED && direction == IndividualCall.Direction.OUTGOING) continue - // Without call id - individualCalls.add( - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = sentTime++, - sms = false, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - individualCall = IndividualCall( - callId = null, - type = type, - state = state, - direction = direction - ) - ) - ) - ) - } - } - } - importExport( - *standardFrames, - Recipient( - id = 4, - contact = Contact( - aci = startedAci, - pni = TestRecipientUtils.nextPni().toByteString(), - username = "cool.01", - e164 = 141255501234, - blocked = false, - visibility = Contact.Visibility.VISIBLE, - registered = Contact.Registered(), - profileKey = TestRecipientUtils.generateProfileKey().toByteString(), - profileSharing = true, - profileGivenName = "Alexa", - profileFamilyName = "Kim", - hideStory = true - ) - ), - Recipient( - id = 5, - group = Group( - masterKey = TestRecipientUtils.generateGroupMasterKey().toByteString(), - whitelisted = true, - hideStory = true, - storySendMode = Group.StorySendMode.DEFAULT - ) - ), - Chat( - id = 1, - recipientId = 4, - archived = true, - pinnedOrder = 1, - expirationTimerMs = 1.days.inWholeMilliseconds, - muteUntilMs = System.currentTimeMillis(), - markedUnread = true, - dontNotifyForMentionsIfMuted = true - ), - *individualCalls.toArray() - ) - } - - @Test - fun messageWithOnlyText() { - var dateSent = System.currentTimeMillis() - val sendStatuses = enumerateSendStatuses(alice.id) - val incomingMessageDetails = enumerateIncomingMessageDetails(dateSent + 200) - val outgoingMessages = ArrayList() - val incomingMessages = ArrayList() - for (sendStatus in sendStatuses) { - outgoingMessages.add( - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = dateSent++, - expireStartDate = dateSent + 1000, - expiresInMs = TimeUnit.DAYS.toMillis(2), - sms = false, - outgoing = ChatItem.OutgoingMessageDetails( - sendStatus = listOf(sendStatus) - ), - standardMessage = StandardMessage( - text = Text( - body = "Text only body" - ) - ) - ) - ) - } - dateSent++ - for (incomingDetail in incomingMessageDetails) { - incomingMessages.add( - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSent++, - expireStartDate = dateSent + 1000, - expiresInMs = TimeUnit.DAYS.toMillis(2), - sms = false, - incoming = incomingDetail, - standardMessage = StandardMessage( - text = Text( - body = "Text only body" - ) - ) - ) - ) - } - - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - *outgoingMessages.toArray(), - *incomingMessages.toArray() - ) - } - - @Test - fun messageWithTextMentionsBodyRangesAndReactions() { - val time = System.currentTimeMillis() - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = 100, - expireStartDate = time, - expiresInMs = TimeUnit.DAYS.toMillis(2), - incoming = ChatItem.IncomingMessageDetails( - dateReceived = 105, - dateServerSent = 104, - read = true, - sealedSender = true - ), - standardMessage = StandardMessage( - text = Text( - body = "Hey check this out I love spans!", - bodyRanges = listOf( - BodyRange( - start = 6, - length = 3, - style = BodyRange.Style.BOLD - ), - BodyRange( - start = 10, - length = 3, - style = BodyRange.Style.ITALIC - ), - BodyRange( - start = 14, - length = 3, - style = BodyRange.Style.SPOILER - ), - BodyRange( - start = 18, - length = 3, - style = BodyRange.Style.STRIKETHROUGH - ), - BodyRange( - start = 22, - length = 3, - style = BodyRange.Style.MONOSPACE - ), - BodyRange( - start = 4, - length = 0, - mentionAci = alice.contact!!.aci - ) - ) - ), - reactions = listOf( - Reaction(emoji = "F", authorId = selfRecipient.id, sentTimestamp = 302, receivedTimestamp = 303), - Reaction(emoji = "F", authorId = alice.id, sentTimestamp = 301, receivedTimestamp = 302) - ) - ) - ) - ) - } - - @Test - fun messageWithTextAndQuotes() { - val spans = listOf( - BodyRange( - start = 6, - length = 3, - style = BodyRange.Style.BOLD - ), - BodyRange( - start = 10, - length = 3, - style = BodyRange.Style.ITALIC - ), - BodyRange( - start = 14, - length = 3, - style = BodyRange.Style.SPOILER - ), - BodyRange( - start = 18, - length = 3, - style = BodyRange.Style.STRIKETHROUGH - ), - BodyRange( - start = 22, - length = 3, - style = BodyRange.Style.MONOSPACE - ) - ) - val time = System.currentTimeMillis() - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = 100, - expireStartDate = time, - expiresInMs = TimeUnit.DAYS.toMillis(2), - incoming = ChatItem.IncomingMessageDetails( - dateReceived = 105, - dateServerSent = 104, - read = true, - sealedSender = true - ), - standardMessage = StandardMessage( - text = Text( - body = "Hey check this out I love spans!", - bodyRanges = spans - ) - ) - ), - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = 101, - expireStartDate = time, - expiresInMs = TimeUnit.DAYS.toMillis(2), - incoming = ChatItem.IncomingMessageDetails( - dateReceived = 105, - dateServerSent = 104, - read = true, - sealedSender = true - ), - standardMessage = StandardMessage( - text = Text( - body = "I quoted an existing message" - ), - quote = Quote( - targetSentTimestamp = 100, - authorId = alice.id, - type = Quote.Type.NORMAL, - text = "Hey check this out I love spans!", - bodyRanges = spans - ) - ) - ), - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = 102, - expireStartDate = time, - expiresInMs = TimeUnit.DAYS.toMillis(2), - incoming = ChatItem.IncomingMessageDetails( - dateReceived = 105, - dateServerSent = 104, - read = true, - sealedSender = true - ), - standardMessage = StandardMessage( - text = Text( - body = "I quoted an non-existing message" - ), - quote = Quote( - targetSentTimestamp = 60, - authorId = alice.id, - type = Quote.Type.NORMAL, - text = "Hey check this out I love spans!", - bodyRanges = spans - ) - ) - ) - ) - } - - @Test - fun messagesNearExpirationNotExported() { - val chat = buildChat(alice, 1) - val expirationNotStarted = ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = 101, - expireStartDate = 0, - expiresInMs = TimeUnit.DAYS.toMillis(1), - sms = false, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = 100, - dateServerSent = 100, - read = true - ), - standardMessage = StandardMessage( - text = Text( - body = "Expiration not started but less than or equal to 1 day" - ) - ) - ) - val importData = exportFrames( - *standardFrames, - alice, - chat, - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = 100, - expireStartDate = System.currentTimeMillis(), - expiresInMs = TimeUnit.DAYS.toMillis(1), - sms = false, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = 100, - dateServerSent = 100, - read = true - ), - standardMessage = StandardMessage( - text = Text( - body = "Near expiration" - ) - ) - ), - expirationNotStarted - ) - import(importData) - val exported = BackupRepository.debugExport() - val expected = exportFrames( - *standardFrames, - alice, - chat, - expirationNotStarted - ) - compare(expected, exported) - } - - @Test - fun messageWithAttachmentsAndQuoteAttachments() { - var dateSent = System.currentTimeMillis() - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = dateSent++, - sms = false, - outgoing = ChatItem.OutgoingMessageDetails( - sendStatus = listOf(SendStatus(alice.id, deliveryStatus = SendStatus.Status.READ, lastStatusUpdateTimestamp = -1)) - ), - standardMessage = StandardMessage( - attachments = listOf( - MessageAttachment( - pointer = FilePointer( - attachmentLocator = FilePointer.AttachmentLocator( - cdnKey = "coolCdnKey", - cdnNumber = 2, - uploadTimestamp = System.currentTimeMillis(), - key = (1..32).map { it.toByte() }.toByteArray().toByteString(), - size = 12345, - digest = (1..32).map { it.toByte() }.toByteArray().toByteString() - ), - contentType = "image/png", - fileName = "very_cool_picture.png", - width = 100, - height = 200, - caption = "Love this cool picture!", - incrementalMacChunkSize = 0 - ), - wasDownloaded = true - ), - MessageAttachment( - pointer = FilePointer( - invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator(), - contentType = "image/png", - width = 100, - height = 200, - caption = "Love this cool picture! Too bad u cant download it", - incrementalMacChunkSize = 0 - ), - wasDownloaded = false - ) - ) - ) - ) - ) - } - - @Test - fun linkPreviewMessages() { - var dateSent = System.currentTimeMillis() - val sendStatuses = enumerateSendStatuses(alice.id) - val incomingMessageDetails = enumerateIncomingMessageDetails(dateSent + 200) - val outgoingMessages = ArrayList() - val incomingMessages = ArrayList() - for (sendStatus in sendStatuses) { - outgoingMessages.add( - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = dateSent++, - expireStartDate = dateSent + 1000, - expiresInMs = TimeUnit.DAYS.toMillis(2), - sms = false, - outgoing = ChatItem.OutgoingMessageDetails( - sendStatus = listOf(sendStatus) - ), - standardMessage = StandardMessage( - text = Text( - body = "Text only body" - ), - linkPreview = listOf( - LinkPreview( - url = "https://signal.org/", - title = "Signal Messenger: Speak Freely", - description = "Say \"hello\" to a different messaging experience. An unexpected focus on privacy, combined with all the features you expect.", - date = System.currentTimeMillis(), - image = FilePointer( - invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator(), - contentType = "image/png", - width = 100, - height = 200, - caption = "Love this cool picture! Too bad u cant download it", - incrementalMacChunkSize = 0 - ) - ) - ) - ) - ) - ) - } - dateSent++ - for (incomingDetail in incomingMessageDetails) { - incomingMessages.add( - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSent++, - expireStartDate = dateSent + 1000, - expiresInMs = TimeUnit.DAYS.toMillis(2), - sms = false, - incoming = incomingDetail, - standardMessage = StandardMessage( - text = Text( - body = "Text only body" - ), - linkPreview = listOf( - LinkPreview( - url = "https://signal.org/", - title = "Signal Messenger: Speak Freely", - description = "Say \"hello\" to a different messaging experience. An unexpected focus on privacy, combined with all the features you expect.", - date = System.currentTimeMillis(), - image = FilePointer( - invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator(), - contentType = "image/png", - width = 100, - height = 200, - caption = "Love this cool picture! Too bad u cant download it", - incrementalMacChunkSize = 0 - ) - ) - ) - ) - ) - ) - } - - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - *outgoingMessages.toArray(), - *incomingMessages.toArray() - ) - } - - @Test - fun contactMessageWithAllFields() { - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = 150L, - sms = false, - outgoing = ChatItem.OutgoingMessageDetails( - sendStatus = listOf(SendStatus(alice.id, deliveryStatus = SendStatus.Status.READ, lastStatusUpdateTimestamp = -1)) - ), - contactMessage = ContactMessage( - contact = listOf( - ContactAttachment( - name = ContactAttachment.Name( - givenName = "Given", - familyName = "Family", - prefix = "Prefix", - suffix = "Suffix", - middleName = "Middle", - displayName = "Display Name" - ), - organization = "Organization", - email = listOf( - ContactAttachment.Email( - value_ = "coolemail@gmail.com", - label = "Label", - type = ContactAttachment.Email.Type.HOME - ), - ContactAttachment.Email( - value_ = "coolemail2@gmail.com", - label = "Label2", - type = ContactAttachment.Email.Type.MOBILE - ) - ), - address = listOf( - ContactAttachment.PostalAddress( - type = ContactAttachment.PostalAddress.Type.HOME, - label = "Label", - street = "Street", - pobox = "POBOX", - neighborhood = "Neighborhood", - city = "City", - region = "Region", - postcode = "15213", - country = "United States" - ) - ), - number = listOf( - ContactAttachment.Phone( - value_ = "+14155551234", - type = ContactAttachment.Phone.Type.CUSTOM, - label = "Label" - ) - ), - avatar = FilePointer( - attachmentLocator = FilePointer.AttachmentLocator( - cdnKey = "coolCdnKey", - cdnNumber = 2, - uploadTimestamp = System.currentTimeMillis(), - key = (1..32).map { it.toByte() }.toByteArray().toByteString(), - size = 12345, - digest = (1..32).map { it.toByte() }.toByteArray().toByteString() - ), - contentType = "image/png", - fileName = "very_cool_picture.png", - width = 100, - height = 200, - caption = "Love this cool picture!", - incrementalMacChunkSize = 0 - ) - ) - ) - ) - ) - ) - } - - @Test - fun simpleChatUpdateMessage() { - var dateSentStart = 100L - val updateMessages = ArrayList() - for (i in 1..11) { - updateMessages.add( - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSentStart++, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - simpleUpdate = SimpleChatUpdate( - type = SimpleChatUpdate.Type.fromValue(i)!! - ) - ) - ) - ) - } - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - *updateMessages.toArray() - ) - } - - @Test - fun expirationTimerUpdateMessage() { - var dateSentStart = 100L - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSentStart++, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - expirationTimerChange = ExpirationTimerChatUpdate( - 1000 - ) - ) - ), - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = dateSentStart++, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - expirationTimerChange = ExpirationTimerChatUpdate( - 0 - ) - ) - ), - ChatItem( - chatId = 1, - authorId = selfRecipient.id, - dateSent = dateSentStart++, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - expirationTimerChange = ExpirationTimerChatUpdate( - 10000 - ) - ) - ), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSentStart++, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - expirationTimerChange = ExpirationTimerChatUpdate( - 0 - ) - ) - ) - ) - } - - @Test - fun profileChangeChatUpdateMessage() { - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = 100L, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - profileChange = ProfileChangeChatUpdate( - previousName = "Aliceee Kim", - newName = "Alice Kim" - ) - ) - ) - ) - } - - @Test - fun threadMergeChatUpdate() { - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = 100L, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - threadMerge = ThreadMergeChatUpdate( - previousE164 = 141255501237 - ) - ) - ) - ) - } - - @Test - fun sessionSwitchoverChatUpdate() { - var dateSentStart = 100L - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSentStart, - directionless = ChatItem.DirectionlessMessageDetails(), - updateMessage = ChatUpdateMessage( - sessionSwitchover = SessionSwitchoverChatUpdate( - e164 = 141255501237 - ) - ) - ) - ) - } - - @Test - fun giftBadgeMessage() { - var dateSentStart = 100L - importExport( - *standardFrames, - alice, - buildChat(alice, 1), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSentStart++, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = dateSentStart, - dateServerSent = dateSentStart, - read = true, - sealedSender = true - ), - giftBadge = GiftBadge( - receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(), - state = GiftBadge.State.OPENED - ) - ), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSentStart++, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = dateSentStart, - dateServerSent = dateSentStart, - read = true, - sealedSender = true - ), - giftBadge = GiftBadge( - receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(), - state = GiftBadge.State.FAILED - ) - ), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSentStart++, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = dateSentStart, - dateServerSent = dateSentStart, - read = true, - sealedSender = true - ), - giftBadge = GiftBadge( - receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(), - state = GiftBadge.State.REDEEMED - ) - ), - ChatItem( - chatId = 1, - authorId = alice.id, - dateSent = dateSentStart++, - incoming = ChatItem.IncomingMessageDetails( - dateReceived = dateSentStart, - dateServerSent = dateSentStart, - read = true, - sealedSender = true - ), - giftBadge = GiftBadge( - receiptCredentialPresentation = Util.getSecretBytes(32).toByteString(), - state = GiftBadge.State.UNOPENED - ) - ) - ) - } - - fun enumerateIncomingMessageDetails(dateSent: Long): List { - val details = mutableListOf() - details.add( - ChatItem.IncomingMessageDetails( - dateReceived = dateSent + 1, - dateServerSent = dateSent, - read = true, - sealedSender = true - ) - ) - details.add( - ChatItem.IncomingMessageDetails( - dateReceived = dateSent + 1, - dateServerSent = dateSent, - read = true, - sealedSender = false - ) - ) - details.add( - ChatItem.IncomingMessageDetails( - dateReceived = dateSent + 1, - dateServerSent = dateSent, - read = false, - sealedSender = true - ) - ) - details.add( - ChatItem.IncomingMessageDetails( - dateReceived = dateSent + 1, - dateServerSent = dateSent, - read = false, - sealedSender = false - ) - ) - return details - } - - fun enumerateSendStatuses(recipientId: Long): List { - val statuses = ArrayList() - val sealedSenderStates = listOf(true, false) - for (sealedSender in sealedSenderStates) { - statuses.add( - SendStatus( - recipientId = recipientId, - deliveryStatus = SendStatus.Status.DELIVERED, - sealedSender = sealedSender, - lastStatusUpdateTimestamp = -1 - ) - ) - statuses.add( - SendStatus( - recipientId = recipientId, - deliveryStatus = SendStatus.Status.PENDING, - sealedSender = sealedSender, - lastStatusUpdateTimestamp = -1, - networkFailure = true - ) - ) - statuses.add( - SendStatus( - recipientId = recipientId, - deliveryStatus = SendStatus.Status.SENT, - sealedSender = sealedSender, - lastStatusUpdateTimestamp = -1 - ) - ) - statuses.add( - SendStatus( - recipientId = recipientId, - deliveryStatus = SendStatus.Status.READ, - sealedSender = sealedSender, - lastStatusUpdateTimestamp = -1 - ) - ) - statuses.add( - SendStatus( - recipientId = recipientId, - deliveryStatus = SendStatus.Status.PENDING, - sealedSender = sealedSender, - networkFailure = true, - lastStatusUpdateTimestamp = -1 - ) - ) - statuses.add( - SendStatus( - recipientId = recipientId, - deliveryStatus = SendStatus.Status.FAILED, - sealedSender = sealedSender, - identityKeyMismatch = true, - lastStatusUpdateTimestamp = -1 - ) - ) - } - return statuses - } - - private fun buildChat(recipient: Recipient, id: Long): Chat { - return Chat( - id = id, - recipientId = recipient.id, - archived = false, - pinnedOrder = 0, - expirationTimerMs = 0, - muteUntilMs = 0, - markedUnread = false, - dontNotifyForMentionsIfMuted = false - ) - } - - /** - * Export passed in frames as a backup. Does not automatically include - * any standard frames (e.g. backup header). - */ - private fun exportFrames(vararg objects: Any): ByteArray { - outputBinProto(*objects) - val outputStream = ByteArrayOutputStream() - val writer = EncryptedBackupWriter( - key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(), - aci = SignalStore.account.aci!!, - outputStream = outputStream, - append = { mac -> outputStream.write(mac) } - ) - - writer.use { - writer.writeFrames(*objects) - } - return outputStream.toByteArray() - } - - private fun import(importData: ByteArray) { - BackupRepository.import(length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY)) - } - - private fun validate(importData: ByteArray): MessageBackup.ValidationResult { - val factory = { ByteArrayInputStream(importData) } - val masterKey = SignalStore.svr.getOrCreateMasterKey() - val key = MessageBackupKey(masterKey.serialize(), org.signal.libsignal.protocol.ServiceId.Aci.parseFromBinary(SELF_ACI.toByteArray())) - - return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, factory, importData.size.toLong()) - } - - /** - * Given some [Frame]s, this will do the following: - * - * 1. Write the frames using an [EncryptedBackupWriter] and keep the result in memory (A). - * 2. Import those frames back into the local database. - * 3. Export the state of the local database and keep the result in memory (B). - * 4. Assert that (A) and (B) are identical. Or, in other words, assert that importing and exporting again results in the original backup data. - */ - private fun importExport(vararg objects: Any) { - val originalBackupData = exportFrames(*objects) - - import(originalBackupData) - - val generatedBackupData = BackupRepository.debugExport() - compare(originalBackupData, generatedBackupData) - } - - private fun BackupExportWriter.writeFrames(vararg objects: Any) { - for (obj in objects) { - when (obj) { - is BackupInfo -> write(obj) - is AccountData -> write(Frame(account = obj)) - is Recipient -> write(Frame(recipient = obj)) - is Chat -> write(Frame(chat = obj)) - is ChatItem -> write(Frame(chatItem = obj)) - is AdHocCall -> write(Frame(adHocCall = obj)) - is StickerPack -> write(Frame(stickerPack = obj)) - else -> Assert.fail("invalid object $obj") - } - } - } - - private fun outputBinProto(vararg objects: Any) { - if (!OUTPUT_FILES) return - - val outputStream = ByteArrayOutputStream() - val plaintextWriter = PlainTextBackupWriter( - outputStream = outputStream - ) - - plaintextWriter.use { - it.writeFrames(*objects) - } - - grantPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE) - val dir = File(Environment.getExternalStorageDirectory(), "backup-tests") - if (dir.mkdirs() || dir.exists()) { - FileOutputStream(File(dir, testName.methodName + ".binproto")).use { - it.write(outputStream.toByteArray()) - it.flush() - } - } - } - - private fun compare(import: ByteArray, export: ByteArray) { - val selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY) - val framesImported = readAllFrames(import, selfData) - val framesExported = readAllFrames(export, selfData) - - compareFrameList(framesImported, framesExported) - } - - private fun compareFrameList(framesImported: List, framesExported: List) { - val accountExported = ArrayList() - val accountImported = ArrayList() - val recipientsImported = ArrayList() - val recipientsExported = ArrayList() - val chatsImported = ArrayList() - val chatsExported = ArrayList() - val chatItemsImported = ArrayList() - val chatItemsExported = ArrayList() - val callsImported = ArrayList() - val callsExported = ArrayList() - val stickersImported = ArrayList() - val stickersExported = ArrayList() - - for (f in framesImported) { - when { - f.account != null -> accountExported.add(f.account!!) - f.recipient != null -> recipientsImported.add(f.recipient!!) - f.chat != null -> chatsImported.add(f.chat!!) - f.chatItem != null -> chatItemsImported.add(f.chatItem!!) - f.adHocCall != null -> callsImported.add(f.adHocCall!!) - f.stickerPack != null -> stickersImported.add(f.stickerPack!!) - } - } - - for (f in framesExported) { - when { - f.account != null -> accountImported.add(f.account!!) - f.recipient != null -> { - val frameRecipient = f.recipient!! - if (frameRecipient.distributionList != null && frameRecipient.distributionList!!.distributionId == DistributionId.MY_STORY.asUuid().toByteArray().toByteString()) { - recipientsExported.add(frameRecipient.copy(distributionList = frameRecipient.distributionList!!.copyWithoutMembers())) - } else { - recipientsExported.add(f.recipient!!) - } - } - f.chat != null -> chatsExported.add(f.chat!!) - f.chatItem != null -> chatItemsExported.add(f.chatItem!!) - f.adHocCall != null -> callsExported.add(f.adHocCall!!) - f.stickerPack != null -> stickersExported.add(f.stickerPack!!) - } - } - prettyAssertEquals(accountImported, accountExported) - prettyAssertEquals(recipientsImported, recipientsExported) { it.id } - prettyAssertEquals(chatsImported, chatsExported) { it.id } - prettyAssertEquals(chatItemsImported, chatItemsExported) { it.dateSent } - prettyAssertEquals(callsImported, callsExported) { it.callId } - prettyAssertEquals(stickersImported, stickersExported) { it.packId } - } - - private fun DistributionListItem.copyWithoutMembers(): DistributionListItem { - return this.copy( - distributionList = this.distributionList?.copy( - memberRecipientIds = emptyList() - ) - ) - } - - private inline fun prettyAssertEquals(import: List, export: List) { - Assert.assertEquals(import.size, export.size) - import.zip(export).forEach { (a1, a2) -> - if (a1 != a2) { - Assert.fail("Items do not match:\n\n-- Pretty diff\n${getObjectDiff(a1, a2)}\n-- Full objects\n$a1\n$a2") - } - } - } - - private fun Random.trueWithProbability(prob: Float): Boolean { - return nextFloat() < prob - } - - private inline fun > prettyAssertEquals(import: List, export: List, crossinline selector: (T) -> R?) { - if (import.size != export.size) { - val msg = StringBuilder() - msg.append("There's a different number of items in the lists!\n\n") - - msg.append("Imported:\n") - for (i in import) { - msg.append(i) - msg.append("\n") - } - if (import.isEmpty()) { - msg.append("") - } - msg.append("\n") - msg.append("Exported:\n") - for (i in export) { - msg.append(i) - msg.append("\n") - } - if (export.isEmpty()) { - msg.append("") - } - Assert.fail(msg.toString()) - } - - Assert.assertEquals(import.size, export.size) - val sortedImport = import.sortedBy(selector) - val sortedExport = export.sortedBy(selector) - - prettyAssertEquals(sortedImport, sortedExport) - } - - private fun readAllFrames(import: ByteArray, selfData: BackupRepository.SelfData): List { - val inputFactory = { ByteArrayInputStream(import) } - val frameReader = EncryptedBackupReader( - key = SignalStore.svr.getOrCreateMasterKey().deriveBackupKey(), - aci = selfData.aci, - length = import.size.toLong(), - dataStream = inputFactory - ) - val frames = ArrayList() - while (frameReader.hasNext()) { - frames.add(frameReader.next()) - } - - return frames - } - - private fun grantPermissions(vararg permissions: String?) { - if (!OUTPUT_FILES) return - - val auto: UiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation - for (perm in permissions) { - auto.grantRuntimePermissionAsUser(InstrumentationRegistry.getInstrumentation().targetContext.packageName, perm, android.os.Process.myUserHandle()) - } - } -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTestSuite.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTestSuite.kt deleted file mode 100644 index 6cab906df5..0000000000 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTestSuite.kt +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2024 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.backup.v2 - -import androidx.test.platform.app.InstrumentationRegistry -import com.github.difflib.DiffUtils -import com.github.difflib.UnifiedDiffUtils -import junit.framework.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import org.signal.core.util.Base64 -import org.signal.core.util.StreamUtil -import org.signal.libsignal.messagebackup.ComparableBackup -import org.signal.libsignal.messagebackup.MessageBackup -import org.signal.libsignal.zkgroup.profiles.ProfileKey -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.whispersystems.signalservice.api.kbs.MasterKey -import org.whispersystems.signalservice.api.push.ServiceId -import java.io.ByteArrayInputStream -import java.util.UUID -import kotlin.random.Random - -@RunWith(Parameterized::class) -class ImportExportTestSuite(private val path: String) { - companion object { - val SELF_ACI = ServiceId.ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641")) - val SELF_PNI = ServiceId.PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910")) - const val SELF_E164 = "+10000000000" - val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32)) - val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do") - - const val TESTS_FOLDER = "backupTests" - - @JvmStatic - @Parameterized.Parameters(name = "{0}") - fun data(): Collection> { - val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)!! - return testFiles - .map { arrayOf(it) } - .toList() - } - } - - @Before - fun setup() { - SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234") - SignalStore.account.setE164(SELF_E164) - SignalStore.account.setAci(SELF_ACI) - SignalStore.account.setPni(SELF_PNI) - SignalStore.account.generateAciIdentityKeyIfNecessary() - SignalStore.account.generatePniIdentityKeyIfNecessary() - } - - @Test - fun testBinProto() { - val binProtoBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("${TESTS_FOLDER}/$path").use { - StreamUtil.readFully(it) - } - val importResult = import(binProtoBytes) - assertTrue(importResult is ImportResult.Success) - val success = importResult as ImportResult.Success - - val generatedBackupData = BackupRepository.debugExport(plaintext = true, currentTime = success.backupTime) - assertEquivalent(binProtoBytes, generatedBackupData) - - // Validator expects encrypted data, so we have to export again with encryption to validate - val encryptedBackupData = BackupRepository.debugExport(plaintext = false, currentTime = success.backupTime) - assertPassesValidator(encryptedBackupData) - } - - private fun import(importData: ByteArray): ImportResult { - return BackupRepository.import( - length = importData.size.toLong(), - inputStreamFactory = { ByteArrayInputStream(importData) }, - selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY), - plaintext = true - ) - } - - private fun assertPassesValidator(generatedBackupData: ByteArray) { - BackupRepository.validate( - length = generatedBackupData.size.toLong(), - inputStreamFactory = { ByteArrayInputStream(generatedBackupData) }, - selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY) - ) - } - - private fun assertEquivalent(import: ByteArray, export: ByteArray) { - val importComparable = ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong()) - val exportComparable = ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, export.inputStream(), import.size.toLong()) - - if (importComparable.unknownFieldMessages.isNotEmpty()) { - throw AssertionError("Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}") - } - - if (exportComparable.unknownFieldMessages.isNotEmpty()) { - throw AssertionError("Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}") - } - - val canonicalImport = importComparable.comparableString - val canonicalExport = exportComparable.comparableString - - if (canonicalImport != canonicalExport) { - val importLines = canonicalImport.lines() - val exportLines = canonicalExport.lines() - - val patch = DiffUtils.diff(importLines, exportLines) - val diff = UnifiedDiffUtils.generateUnifiedDiff("Import", "Export", importLines, patch, 3).joinToString(separator = "\n") - - throw AssertionError("Imported backup does not match exported backup. Diff:\n$diff") - } - } -} diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt index 624395ecfd..5f4ef1fcae 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientTableTest_getAndPossiblyMerge.kt @@ -1103,8 +1103,8 @@ class RecipientTableTest_getAndPossiblyMerge { init { // Need to delete these first to prevent foreign key crash - SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list") - SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list_member") + SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}") + SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}") SqlUtil.getAllTables(SignalDatabase.rawDatabase) .filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt index 90fa15aa11..fc9038f0c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt @@ -49,13 +49,14 @@ class ArchivedAttachment : Attachment { stickerLocator: StickerLocator?, gif: Boolean, quote: Boolean, - uuid: UUID? + uuid: UUID?, + fileName: String? ) : super( contentType = contentType ?: "", quote = quote, transferState = AttachmentTable.TRANSFER_NEEDS_RESTORE, size = size, - fileName = null, + fileName = fileName, cdn = Cdn.fromCdnNumber(cdn), remoteLocation = cdnKey, remoteKey = Base64.encodeWithoutPadding(key), diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/Cdn.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/Cdn.kt index 9131944d47..fe7841bdb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/Cdn.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/Cdn.kt @@ -46,7 +46,7 @@ enum class Cdn(private val value: Int) { 0 -> CDN_0 2 -> CDN_2 3 -> CDN_3 - else -> throw UnsupportedOperationException() + else -> throw UnsupportedOperationException("Invalid CDN number: $cdnNumber") } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt index abedc18b98..0e1a16c992 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/DatabaseAttachment.kt @@ -83,7 +83,7 @@ class DatabaseAttachment : Attachment { thumbnailRestoreState: AttachmentTable.ThumbnailRestoreState, uuid: UUID? ) : super( - contentType = contentType!!, + contentType = contentType, transferState = transferProgress, size = size, fileName = fileName, diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt index 552a1071bc..cb3430f86f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/TombstoneAttachment.kt @@ -13,7 +13,7 @@ import java.util.UUID * quote them and know their contentType even though the media has been deleted. */ class TombstoneAttachment : Attachment { - constructor(contentType: String, quote: Boolean) : super( + constructor(contentType: String?, quote: Boolean) : super( contentType = contentType, quote = quote, transferState = AttachmentTable.TRANSFER_PROGRESS_DONE, diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt index 1998161fca..18439eb2e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/UriAttachment.kt @@ -14,7 +14,7 @@ class UriAttachment : Attachment { constructor( uri: Uri, - contentType: String, + contentType: String?, transferState: Int, size: Long, fileName: String?, @@ -50,7 +50,7 @@ class UriAttachment : Attachment { @JvmOverloads constructor( dataUri: Uri, - contentType: String, + contentType: String?, transferState: Int, size: Long, width: Int, 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 781d68097b..61f3565bc7 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 @@ -211,7 +211,7 @@ object BackupRepository { ) } - val exportState = ExportState(backupTime = currentTime, allowMediaBackup = SignalStore.backup.backsUpMedia) + val exportState = ExportState(backupTime = currentTime, mediaBackupEnabled = SignalStore.backup.backsUpMedia) writer.use { writer.write( @@ -302,13 +302,15 @@ object BackupRepository { // Note: Without a transaction, bad imports could lead to lost data. But because we have a transaction, // writes from other threads are blocked. This is something to think more about. SignalDatabase.rawDatabase.withinTransaction { - SignalStore.clearAllDataForBackupRestore() SignalDatabase.recipients.clearAllDataForBackupRestore() SignalDatabase.distributionLists.clearAllDataForBackupRestore() SignalDatabase.threads.clearAllDataForBackupRestore() SignalDatabase.messages.clearAllDataForBackupRestore() SignalDatabase.attachments.clearAllDataForBackupRestore() SignalDatabase.stickers.clearAllDataForBackupRestore() + SignalDatabase.reactions.clearAllDataForBackupRestore() + SignalDatabase.inAppPayments.clearAllDataForBackupRestore() + SignalDatabase.chatColors.clearAllDataForBackupRestore() // Add back self after clearing data val selfId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(selfData.aci, selfData.pni, selfData.e164, pniVerified = true, changeSelf = true) @@ -953,7 +955,7 @@ data class ArchivedMediaObject(val mediaId: String, val cdn: Int) data class BackupDirectories(val backupDir: String, val mediaDir: String) -class ExportState(val backupTime: Long, val allowMediaBackup: Boolean) { +class ExportState(val backupTime: Long, val mediaBackupEnabled: Boolean) { val recipientIds: MutableSet = hashSetOf() val threadIds: MutableSet = hashSetOf() val localToRemoteCustomChatColors: MutableMap = hashMapOf() diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatColorsTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatColorsTableBackupExtensions.kt new file mode 100644 index 0000000000..cdaeb387c2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatColorsTableBackupExtensions.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.database + +import org.signal.core.util.deleteAll +import org.thoughtcrime.securesms.database.ChatColorsTable + +fun ChatColorsTable.clearAllDataForBackupRestore() { + writableDatabase.deleteAll(ChatColorsTable.TABLE_NAME) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt index 26470d9a71..3f03484b04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt @@ -131,7 +131,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: } } - val reactionsById: Map> = SignalDatabase.reactions.getReactionsForMessages(records.keys) + val reactionsById: Map> = SignalDatabase.reactions.getReactionsForMessages(records.keys).map { entry -> entry.key to entry.value.sortedBy { it.dateReceived } }.toMap() val mentionsById: Map> = SignalDatabase.mentions.getMentionsForMessages(records.keys) val attachmentsById: Map> = SignalDatabase.attachments.getAttachmentsForMessages(records.keys) val groupReceiptsById: Map> = SignalDatabase.groupReceipts.getGroupReceiptInfoForMessages(records.keys) @@ -757,28 +757,28 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: private fun DatabaseAttachment.toBackupAttachment(): MessageAttachment { val builder = FilePointer.Builder() - builder.contentType = contentType - builder.incrementalMac = incrementalDigest?.toByteString() - builder.incrementalMacChunkSize = incrementalMacChunkSize - builder.fileName = fileName - builder.width = width - builder.height = height - builder.caption = caption - builder.blurHash = blurHash?.hash + builder.contentType = this.contentType?.takeUnless { it.isBlank() } + builder.incrementalMac = this.incrementalDigest?.toByteString() + builder.incrementalMacChunkSize = this.incrementalMacChunkSize.takeIf { it > 0 } + builder.fileName = this.fileName + builder.width = this.width.takeUnless { it == 0 } + builder.height = this.height.takeUnless { it == 0 } + builder.caption = this.caption + builder.blurHash = this.blurHash?.hash - if (remoteKey.isNullOrBlank() || remoteDigest == null || size == 0L) { + if (this.remoteKey.isNullOrBlank() || this.remoteDigest == null || this.size == 0L) { builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator() } else { if (archiveMedia) { builder.backupLocator = FilePointer.BackupLocator( - mediaName = archiveMediaName ?: this.getMediaName().toString(), - cdnNumber = if (archiveMediaName != null) archiveCdn else Cdn.CDN_3.cdnNumber, // TODO (clark): Update when new proto with optional cdn is landed + mediaName = this.archiveMediaName ?: this.getMediaName().toString(), + cdnNumber = if (this.archiveMediaName != null) this.archiveCdn else Cdn.CDN_3.cdnNumber, // TODO (clark): Update when new proto with optional cdn is landed key = Base64.decode(remoteKey).toByteString(), - size = this.size, - digest = remoteDigest.toByteString() + size = this.size.toInt(), + digest = this.remoteDigest.toByteString() ) } else { - if (remoteLocation.isNullOrBlank()) { + if (this.remoteLocation.isNullOrBlank()) { builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator() } else { builder.attachmentLocator = FilePointer.AttachmentLocator( @@ -787,7 +787,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: uploadTimestamp = this.uploadTimestamp, key = Base64.decode(remoteKey).toByteString(), size = this.size.toInt(), - digest = remoteDigest.toByteString() + digest = this.remoteDigest.toByteString() ) } } @@ -795,16 +795,16 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: return MessageAttachment( pointer = builder.build(), wasDownloaded = this.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || this.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE, - flag = if (voiceNote) { + flag = if (this.voiceNote) { MessageAttachment.Flag.VOICE_MESSAGE - } else if (videoGif) { + } else if (this.videoGif) { MessageAttachment.Flag.GIF - } else if (borderless) { + } else if (this.borderless) { MessageAttachment.Flag.BORDERLESS } else { MessageAttachment.Flag.NONE }, - clientUuid = uuid?.let { UuidUtil.toByteString(uuid) } + clientUuid = this.uuid?.let { UuidUtil.toByteString(uuid) } ) } @@ -873,11 +873,18 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: } return decoded.ranges.map { + val mention = it.mentionUuid?.let { uuid -> UuidUtil.parseOrThrow(uuid) }?.toByteArray()?.toByteString() + val style = if (mention == null) { + it.style?.toBackupBodyRangeStyle() ?: BackupBodyRange.Style.NONE + } else { + null + } + BackupBodyRange( start = it.start, length = it.length, - mentionAci = it.mentionUuid?.let { uuid -> UuidUtil.parseOrThrow(uuid) }?.toByteArray()?.toByteString(), - style = it.style?.toBackupBodyRangeStyle() + mentionAci = mention, + style = style ) } } @@ -899,7 +906,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: emoji = it.emoji, authorId = it.author.toLong(), sentTimestamp = it.dateSent, - receivedTimestamp = it.dateReceived + receivedTimestamp = it.dateReceived, + sortOrder = 0 // TODO [backup] make this it.dateReceived once comparator support is added ) } ?: emptyList() } @@ -913,48 +921,98 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: return groupReceipts.toBackupSendStatus(this.networkFailureRecipientIds, this.identityMismatchRecipientIds) } - val status: SendStatus.Status = when { - this.viewed -> SendStatus.Status.VIEWED - this.hasReadReceipt -> SendStatus.Status.READ - this.hasDeliveryReceipt -> SendStatus.Status.DELIVERED - this.baseType == MessageTypes.BASE_SENT_TYPE -> SendStatus.Status.SENT - MessageTypes.isFailedMessageType(this.type) -> SendStatus.Status.FAILED - else -> SendStatus.Status.PENDING + val statusBuilder = SendStatus.Builder() + .recipientId(this.toRecipientId) + .timestamp(this.receiptTimestamp) + + when { + this.identityMismatchRecipientIds.contains(this.toRecipientId) -> { + statusBuilder.failed = SendStatus.Failed( + identityKeyMismatch = true + ) + } + this.networkFailureRecipientIds.contains(this.toRecipientId) -> { + statusBuilder.failed = SendStatus.Failed( + network = true + ) + } + this.baseType == MessageTypes.BASE_SENT_TYPE -> { + statusBuilder.sent = SendStatus.Sent( + sealedSender = this.sealedSender + ) + } + this.hasDeliveryReceipt -> { + statusBuilder.delivered = SendStatus.Delivered( + sealedSender = this.sealedSender + ) + } + this.hasReadReceipt -> { + statusBuilder.read = SendStatus.Read( + sealedSender = this.sealedSender + ) + } + this.viewed -> { + statusBuilder.viewed = SendStatus.Viewed( + sealedSender = this.sealedSender + ) + } + else -> { + statusBuilder.pending = SendStatus.Pending() + } } - return listOf( - SendStatus( - recipientId = this.toRecipientId, - deliveryStatus = status, - lastStatusUpdateTimestamp = this.receiptTimestamp, - sealedSender = this.sealedSender, - networkFailure = this.networkFailureRecipientIds.contains(this.toRecipientId), - identityKeyMismatch = this.identityMismatchRecipientIds.contains(this.toRecipientId) - ) - ) + return listOf(statusBuilder.build()) } private fun List.toBackupSendStatus(networkFailureRecipientIds: Set, identityMismatchRecipientIds: Set): List { return this.map { - SendStatus( - recipientId = it.recipientId.toLong(), - deliveryStatus = it.status.toBackupDeliveryStatus(), - sealedSender = it.isUnidentified, - lastStatusUpdateTimestamp = it.timestamp, - networkFailure = networkFailureRecipientIds.contains(it.recipientId.toLong()), - identityKeyMismatch = identityMismatchRecipientIds.contains(it.recipientId.toLong()) - ) - } - } + val statusBuilder = SendStatus.Builder() + .recipientId(it.recipientId.toLong()) + .timestamp(it.timestamp) - private fun Int.toBackupDeliveryStatus(): SendStatus.Status { - return when (this) { - GroupReceiptTable.STATUS_UNDELIVERED -> SendStatus.Status.PENDING - GroupReceiptTable.STATUS_DELIVERED -> SendStatus.Status.DELIVERED - GroupReceiptTable.STATUS_READ -> SendStatus.Status.READ - GroupReceiptTable.STATUS_VIEWED -> SendStatus.Status.VIEWED - GroupReceiptTable.STATUS_SKIPPED -> SendStatus.Status.SKIPPED - else -> SendStatus.Status.SKIPPED + when { + identityMismatchRecipientIds.contains(it.recipientId.toLong()) -> { + statusBuilder.failed = SendStatus.Failed( + identityKeyMismatch = true + ) + } + networkFailureRecipientIds.contains(it.recipientId.toLong()) -> { + statusBuilder.failed = SendStatus.Failed( + network = true + ) + } + it.status == GroupReceiptTable.STATUS_UNKNOWN -> { + statusBuilder.pending = SendStatus.Pending() + } + it.status == GroupReceiptTable.STATUS_UNDELIVERED -> { + statusBuilder.sent = SendStatus.Sent( + sealedSender = it.isUnidentified + ) + } + it.status == GroupReceiptTable.STATUS_DELIVERED -> { + statusBuilder.delivered = SendStatus.Delivered( + sealedSender = it.isUnidentified + ) + } + it.status == GroupReceiptTable.STATUS_READ -> { + statusBuilder.read = SendStatus.Read( + sealedSender = it.isUnidentified + ) + } + it.status == GroupReceiptTable.STATUS_VIEWED -> { + statusBuilder.viewed = SendStatus.Viewed( + sealedSender = it.isUnidentified + ) + } + it.status == GroupReceiptTable.STATUS_SKIPPED -> { + statusBuilder.skipped = SendStatus.Skipped() + } + else -> { + statusBuilder.pending = SendStatus.Pending() + } + } + + statusBuilder.build() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt index f2d6193874..2922164555 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemImportInserter.kt @@ -11,10 +11,12 @@ import okio.ByteString import org.signal.core.util.Base64 import org.signal.core.util.Hex import org.signal.core.util.SqlUtil +import org.signal.core.util.forEach import org.signal.core.util.logging.Log import org.signal.core.util.orNull import org.signal.core.util.requireLong import org.signal.core.util.toInt +import org.signal.core.util.update import org.thoughtcrime.securesms.attachments.ArchivedAttachment import org.thoughtcrime.securesms.attachments.Attachment import org.thoughtcrime.securesms.attachments.Cdn @@ -211,17 +213,13 @@ class ChatItemImportInserter( if (buffer.size == 0) { return false } - buildBulkInsert(MessageTable.TABLE_NAME, MESSAGE_COLUMNS, buffer.messages).forEach { - db.rawQuery("${it.query.where} RETURNING ${MessageTable.ID}", it.query.whereArgs).use { cursor -> - var index = 0 - while (cursor.moveToNext()) { - val rowId = cursor.requireLong(MessageTable.ID) - val followup = it.inserts[index].followUp - if (followup != null) { - followup(rowId) - } - index++ - } + + var messageInsertIndex = 0 + SqlUtil.buildBulkInsert(MessageTable.TABLE_NAME, MESSAGE_COLUMNS, buffer.messages.map { it.contentValues }).forEach { query -> + db.rawQuery("${query.where} RETURNING ${MessageTable.ID}", query.whereArgs).forEach { cursor -> + val finalMessageId = cursor.requireLong(MessageTable.ID) + val relatedInsert = buffer.messages[messageInsertIndex++] + relatedInsert.followUp?.invoke(finalMessageId) } } @@ -240,15 +238,6 @@ class ChatItemImportInserter( return true } - private fun buildBulkInsert(tableName: String, columns: Array, messageInserts: List, maxQueryArgs: Int = 999): List { - val batchSize = maxQueryArgs / columns.size - - return messageInserts - .chunked(batchSize) - .map { batch: List -> BatchInsert(batch, SqlUtil.buildSingleBulkInsert(tableName, columns, batch.map { it.contentValues })) } - .toList() - } - private fun ChatItem.toMessageInsert(fromRecipientId: RecipientId, chatRecipientId: RecipientId, threadId: Long): MessageInsert { val contentValues = this.toMessageContentValues(fromRecipientId, chatRecipientId, threadId) @@ -304,22 +293,22 @@ class ChatItemImportInserter( } } } + if (this.paymentNotification != null) { followUp = { messageRowId -> val uuid = tryRestorePayment(this, chatRecipientId) if (uuid != null) { - db.update( - MessageTable.TABLE_NAME, - contentValuesOf( + db.update(MessageTable.TABLE_NAME) + .values( MessageTable.BODY to uuid.toString(), MessageTable.TYPE to ((contentValues.getAsLong(MessageTable.TYPE) and MessageTypes.SPECIAL_TYPES_MASK.inv()) or MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION) - ), - "${MessageTable.ID}=?", - SqlUtil.buildArgs(messageRowId) - ) + ) + .where("${MessageTable.ID} = ?", messageRowId) + .run() } } } + if (this.contactMessage != null) { val contacts = this.contactMessage.contact.map { backupContact -> Contact( @@ -352,9 +341,10 @@ class ChatItemImportInserter( address.country ) }, - Contact.Avatar(null, backupContact.avatar.toLocalAttachment(voiceNote = false, borderless = false, gif = false, wasDownloaded = true), true) + Contact.Avatar(null, backupContact.avatar.toLocalAttachment(), true) ) } + val contactAttachments = contacts.mapNotNull { it.avatarAttachment } if (contacts.isNotEmpty()) { followUp = { messageRowId -> @@ -374,6 +364,7 @@ class ChatItemImportInserter( } } } + if (this.standardMessage != null) { val bodyRanges = this.standardMessage.text?.bodyRanges if (!bodyRanges.isNullOrEmpty()) { @@ -399,9 +390,11 @@ class ChatItemImportInserter( val attachments = this.standardMessage.attachments.mapNotNull { attachment -> attachment.toLocalAttachment() } + val quoteAttachments = this.standardMessage.quote?.attachments?.mapNotNull { it.toLocalAttachment() } ?: emptyList() + if (attachments.isNotEmpty() || linkPreviewAttachments.isNotEmpty() || quoteAttachments.isNotEmpty()) { followUp = { messageRowId -> val attachmentMap = SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, attachments + linkPreviewAttachments, quoteAttachments) @@ -418,6 +411,7 @@ class ChatItemImportInserter( } } } + if (this.stickerMessage != null) { val sticker = this.stickerMessage.sticker val attachment = sticker.toLocalAttachment() @@ -427,6 +421,7 @@ class ChatItemImportInserter( } } } + return MessageInsert(contentValues, followUp) } @@ -442,7 +437,7 @@ class ChatItemImportInserter( contentValues.put(MessageTable.TO_RECIPIENT_ID, (if (this.outgoing != null) chatRecipientId else selfId).serialize()) contentValues.put(MessageTable.THREAD_ID, threadId) contentValues.put(MessageTable.DATE_RECEIVED, this.incoming?.dateReceived ?: this.dateSent) - contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOfOrNull { it.lastStatusUpdateTimestamp } ?: 0) + contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOfOrNull { it.timestamp } ?: 0) contentValues.putNull(MessageTable.LATEST_REVISION_ID) contentValues.putNull(MessageTable.ORIGINAL_MESSAGE_ID) contentValues.put(MessageTable.REVISION_NUMBER, 0) @@ -450,9 +445,9 @@ class ChatItemImportInserter( contentValues.put(MessageTable.EXPIRE_STARTED, this.expireStartDate ?: 0) if (this.outgoing != null) { - val viewed = this.outgoing.sendStatus.any { it.deliveryStatus == SendStatus.Status.VIEWED } - val hasReadReceipt = viewed || this.outgoing.sendStatus.any { it.deliveryStatus == SendStatus.Status.READ } - val hasDeliveryReceipt = viewed || hasReadReceipt || this.outgoing.sendStatus.any { it.deliveryStatus == SendStatus.Status.DELIVERED } + val viewed = this.outgoing.sendStatus.any { it.viewed != null } + val hasReadReceipt = viewed || this.outgoing.sendStatus.any { it.read != null } + val hasDeliveryReceipt = viewed || hasReadReceipt || this.outgoing.sendStatus.any { it.delivered != null } contentValues.put(MessageTable.VIEWED_COLUMN, viewed.toInt()) contentValues.put(MessageTable.HAS_READ_RECEIPT, hasReadReceipt.toInt()) @@ -536,7 +531,7 @@ class ChatItemImportInserter( ReactionTable.MESSAGE_ID to messageId, ReactionTable.AUTHOR_ID to authorId, ReactionTable.DATE_SENT to it.sentTimestamp, - ReactionTable.DATE_RECEIVED to it.receivedTimestamp, + ReactionTable.DATE_RECEIVED to (it.receivedTimestamp ?: it.sortOrder), ReactionTable.EMOJI to it.emoji ) } else { @@ -551,7 +546,7 @@ class ChatItemImportInserter( return emptyList() } - // TODO This seems like an indirect/bad way to detect if this is a 1:1 or group convo + // TODO [backup] This seems like an indirect/bad way to detect if this is a 1:1 or group convo if (this.outgoing.sendStatus.size == 1 && this.outgoing.sendStatus[0].recipientId == chatBackupRecipientId) { return emptyList() } @@ -563,8 +558,8 @@ class ChatItemImportInserter( contentValuesOf( GroupReceiptTable.MMS_ID to messageId, GroupReceiptTable.RECIPIENT_ID to recipientId.serialize(), - GroupReceiptTable.STATUS to sendStatus.deliveryStatus.toLocalSendStatus(), - GroupReceiptTable.TIMESTAMP to sendStatus.lastStatusUpdateTimestamp, + GroupReceiptTable.STATUS to sendStatus.toLocalSendStatus(), + GroupReceiptTable.TIMESTAMP to sendStatus.timestamp, GroupReceiptTable.UNIDENTIFIED to sendStatus.sealedSender ) } else { @@ -576,9 +571,9 @@ class ChatItemImportInserter( private fun ChatItem.getMessageType(): Long { var type: Long = if (this.outgoing != null) { - if (this.outgoing.sendStatus.count { it.identityKeyMismatch } > 0) { + if (this.outgoing.sendStatus.count { it.failed?.identityKeyMismatch == true } > 0) { MessageTypes.BASE_SENT_FAILED_TYPE - } else if (this.outgoing.sendStatus.count { it.networkFailure } > 0) { + } else if (this.outgoing.sendStatus.count { it.failed?.network == true } > 0) { MessageTypes.BASE_SENDING_TYPE } else { MessageTypes.BASE_SENT_TYPE @@ -632,6 +627,7 @@ class ChatItemImportInserter( SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST or typeWithoutBase SimpleChatUpdate.Type.UNSUPPORTED_PROTOCOL_MESSAGE -> MessageTypes.UNSUPPORTED_MESSAGE_TYPE or typeWithoutBase SimpleChatUpdate.Type.REPORTED_SPAM -> MessageTypes.SPECIAL_TYPE_REPORTED_SPAM or typeWithoutBase + else -> throw NotImplementedError() } } updateMessage.expirationTimerChange != null -> { @@ -846,7 +842,7 @@ class ChatItemImportInserter( } val networkFailures = chatItem.outgoing.sendStatus - .filter { status -> status.networkFailure } + .filter { status -> status.failed?.network ?: false } .mapNotNull { status -> importState.remoteToLocalRecipientId[status.recipientId] } .map { recipientId -> NetworkFailure(recipientId) } .toSet() @@ -862,7 +858,7 @@ class ChatItemImportInserter( } val mismatches = chatItem.outgoing.sendStatus - .filter { status -> status.identityKeyMismatch } + .filter { status -> status.failed?.identityKeyMismatch ?: false } .mapNotNull { status -> importState.remoteToLocalRecipientId[status.recipientId] } .map { recipientId -> IdentityKeyMismatch(recipientId, null) } // TODO We probably want the actual identity key in this status situation? .toSet() @@ -898,101 +894,73 @@ class ChatItemImportInserter( ) } - private fun SendStatus.Status.toLocalSendStatus(): Int { - return when (this) { - SendStatus.Status.UNKNOWN -> GroupReceiptTable.STATUS_UNKNOWN - SendStatus.Status.FAILED -> GroupReceiptTable.STATUS_UNKNOWN - SendStatus.Status.PENDING -> GroupReceiptTable.STATUS_UNDELIVERED - SendStatus.Status.SENT -> GroupReceiptTable.STATUS_UNDELIVERED - SendStatus.Status.DELIVERED -> GroupReceiptTable.STATUS_DELIVERED - SendStatus.Status.READ -> GroupReceiptTable.STATUS_READ - SendStatus.Status.VIEWED -> GroupReceiptTable.STATUS_VIEWED - SendStatus.Status.SKIPPED -> GroupReceiptTable.STATUS_SKIPPED + private fun SendStatus.toLocalSendStatus(): Int { + return when { + this.pending != null -> GroupReceiptTable.STATUS_UNKNOWN + this.sent != null -> GroupReceiptTable.STATUS_UNDELIVERED + this.delivered != null -> GroupReceiptTable.STATUS_DELIVERED + this.read != null -> GroupReceiptTable.STATUS_READ + this.viewed != null -> GroupReceiptTable.STATUS_VIEWED + this.skipped != null -> GroupReceiptTable.STATUS_SKIPPED + this.failed != null -> GroupReceiptTable.STATUS_UNKNOWN + else -> GroupReceiptTable.STATUS_UNKNOWN } } - private fun FilePointer?.toLocalAttachment(voiceNote: Boolean, borderless: Boolean, gif: Boolean, wasDownloaded: Boolean, stickerLocator: StickerLocator? = null, contentType: String? = this?.contentType, fileName: String? = this?.fileName, uuid: ByteString? = null): Attachment? { - if (this == null) return null - - if (attachmentLocator != null) { - val signalAttachmentPointer = SignalServiceAttachmentPointer( - attachmentLocator.cdnNumber, - SignalServiceAttachmentRemoteId.from(attachmentLocator.cdnKey), - contentType, - attachmentLocator.key.toByteArray(), - Optional.ofNullable(attachmentLocator.size), - Optional.empty(), - width ?: 0, - height ?: 0, - Optional.ofNullable(attachmentLocator.digest.toByteArray()), - Optional.ofNullable(incrementalMac?.toByteArray()), - incrementalMacChunkSize ?: 0, - Optional.ofNullable(fileName), - voiceNote, - borderless, - gif, - Optional.ofNullable(caption), - Optional.ofNullable(blurHash), - attachmentLocator.uploadTimestamp, - UuidUtil.fromByteStringOrNull(uuid) - ) - return PointerAttachment.forPointer( - pointer = Optional.of(signalAttachmentPointer), - stickerLocator = stickerLocator, - transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING - ).orNull() - } else if (invalidAttachmentLocator != null) { - return TombstoneAttachment( - contentType = contentType, - incrementalMac = incrementalMac?.toByteArray(), - incrementalMacChunkSize = incrementalMacChunkSize, - width = width, - height = height, - caption = caption, - blurHash = blurHash, - voiceNote = voiceNote, - borderless = borderless, - gif = gif, - quote = false, - uuid = UuidUtil.fromByteStringOrNull(uuid) - ) - } else if (backupLocator != null) { - return ArchivedAttachment( - contentType = contentType, - size = backupLocator.size.toLong(), - cdn = backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber, - key = backupLocator.key.toByteArray(), - cdnKey = backupLocator.transitCdnKey, - archiveCdn = backupLocator.cdnNumber, - archiveMediaName = backupLocator.mediaName, - archiveMediaId = importState.backupKey.deriveMediaId(MediaName(backupLocator.mediaName)).encode(), - archiveThumbnailMediaId = importState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(backupLocator.mediaName)).encode(), - digest = backupLocator.digest.toByteArray(), - incrementalMac = incrementalMac?.toByteArray(), - incrementalMacChunkSize = incrementalMacChunkSize, - width = width, - height = height, - caption = caption, - blurHash = blurHash, - voiceNote = voiceNote, - borderless = borderless, - gif = gif, - quote = false, - stickerLocator = stickerLocator, - uuid = UuidUtil.fromByteStringOrNull(uuid) - ) + private val SendStatus.sealedSender: Boolean + get() { + return this.sent?.sealedSender + ?: this.delivered?.sealedSender + ?: this.read?.sealedSender + ?: this.viewed?.sealedSender + ?: false + } + + private fun LinkPreview.toLocalLinkPreview(): org.thoughtcrime.securesms.linkpreview.LinkPreview { + return org.thoughtcrime.securesms.linkpreview.LinkPreview( + this.url, + this.title ?: "", + this.description ?: "", + this.date ?: 0, + Optional.ofNullable(this.image?.toLocalAttachment()) + ) + } + + private fun MessageAttachment.toLocalAttachment(contentType: String? = this.pointer?.contentType, fileName: String? = this.pointer?.fileName): Attachment? { + return this.pointer?.toLocalAttachment( + voiceNote = this.flag == MessageAttachment.Flag.VOICE_MESSAGE, + borderless = this.flag == MessageAttachment.Flag.BORDERLESS, + gif = this.flag == MessageAttachment.Flag.GIF, + wasDownloaded = this.wasDownloaded, + stickerLocator = null, + contentType = contentType, + fileName = fileName, + uuid = this.clientUuid + ) + } + + private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? { + val thumbnail = this.thumbnail?.toLocalAttachment(this.contentType, this.fileName) + + return if (thumbnail != null) { + thumbnail + } else if (this.contentType == null) { + null + } else { + PointerAttachment.forPointer( + quotedAttachment = DataMessage.Quote.QuotedAttachment( + contentType = this.contentType, + fileName = this.fileName, + thumbnail = null + ) + ).orNull() } - return null } private fun Sticker?.toLocalAttachment(): Attachment? { if (this == null) return null return data_.toLocalAttachment( - voiceNote = false, - gif = false, - borderless = false, - wasDownloaded = true, stickerLocator = StickerLocator( packId = Hex.toStringCondensed(packId.toByteArray()), packKey = Hex.toStringCondensed(packKey.toByteArray()), @@ -1002,24 +970,89 @@ class ChatItemImportInserter( ) } - private fun LinkPreview.toLocalLinkPreview(): org.thoughtcrime.securesms.linkpreview.LinkPreview { - return org.thoughtcrime.securesms.linkpreview.LinkPreview( - this.url, - this.title ?: "", - this.description ?: "", - this.date ?: 0, - Optional.ofNullable(this.image?.toLocalAttachment(voiceNote = false, borderless = false, gif = false, wasDownloaded = true)) - ) - } - - private fun MessageAttachment.toLocalAttachment(): Attachment? { - return pointer?.toLocalAttachment( - voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE, - gif = flag == MessageAttachment.Flag.GIF, - borderless = flag == MessageAttachment.Flag.BORDERLESS, - wasDownloaded = wasDownloaded, - uuid = clientUuid - ) + private fun FilePointer?.toLocalAttachment( + borderless: Boolean = false, + gif: Boolean = false, + voiceNote: Boolean = false, + wasDownloaded: Boolean = true, + stickerLocator: StickerLocator? = null, + contentType: String? = this?.contentType, + fileName: String? = this?.fileName, + uuid: ByteString? = null + ): Attachment? { + return if (this == null) { + null + } else if (this.attachmentLocator != null) { + val signalAttachmentPointer = SignalServiceAttachmentPointer( + cdnNumber = this.attachmentLocator.cdnNumber, + remoteId = SignalServiceAttachmentRemoteId.from(this.attachmentLocator.cdnKey), + contentType = contentType, + key = this.attachmentLocator.key.toByteArray(), + size = Optional.ofNullable(this.attachmentLocator.size), + preview = Optional.empty(), + width = this.width ?: 0, + height = this.height ?: 0, + digest = Optional.ofNullable(this.attachmentLocator.digest.toByteArray()), + incrementalDigest = Optional.ofNullable(this.incrementalMac?.toByteArray()), + incrementalMacChunkSize = this.incrementalMacChunkSize ?: 0, + fileName = Optional.ofNullable(fileName), + voiceNote = voiceNote, + isBorderless = borderless, + isGif = gif, + caption = Optional.ofNullable(this.caption), + blurHash = Optional.ofNullable(this.blurHash), + uploadTimestamp = this.attachmentLocator.uploadTimestamp, + uuid = UuidUtil.fromByteStringOrNull(uuid) + ) + PointerAttachment.forPointer( + pointer = Optional.of(signalAttachmentPointer), + stickerLocator = stickerLocator, + transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING + ).orNull() + } else if (this.invalidAttachmentLocator != null) { + TombstoneAttachment( + contentType = contentType, + incrementalMac = this.incrementalMac?.toByteArray(), + incrementalMacChunkSize = this.incrementalMacChunkSize, + width = this.width, + height = this.height, + caption = this.caption, + blurHash = this.blurHash, + voiceNote = voiceNote, + borderless = borderless, + gif = gif, + quote = false, + uuid = UuidUtil.fromByteStringOrNull(uuid) + ) + } else if (this.backupLocator != null) { + ArchivedAttachment( + contentType = contentType, + size = this.backupLocator.size.toLong(), + cdn = this.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber, + key = this.backupLocator.key.toByteArray(), + cdnKey = this.backupLocator.transitCdnKey, + archiveCdn = this.backupLocator.cdnNumber, + archiveMediaName = this.backupLocator.mediaName, + archiveMediaId = importState.backupKey.deriveMediaId(MediaName(this.backupLocator.mediaName)).encode(), + archiveThumbnailMediaId = importState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(this.backupLocator.mediaName)).encode(), + digest = this.backupLocator.digest.toByteArray(), + incrementalMac = this.incrementalMac?.toByteArray(), + incrementalMacChunkSize = this.incrementalMacChunkSize, + width = this.width, + height = this.height, + caption = this.caption, + blurHash = this.blurHash, + voiceNote = voiceNote, + borderless = borderless, + gif = gif, + quote = false, + stickerLocator = stickerLocator, + uuid = UuidUtil.fromByteStringOrNull(uuid), + fileName = fileName + ) + } else { + null + } } private fun ContactAttachment.Name?.toLocal(): Contact.Name { @@ -1058,23 +1091,6 @@ class ChatItemImportInserter( } } - private fun MessageAttachment.toLocalAttachment(contentType: String?, fileName: String?): Attachment? { - return pointer?.toLocalAttachment( - voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE, - gif = flag == MessageAttachment.Flag.GIF, - borderless = flag == MessageAttachment.Flag.BORDERLESS, - wasDownloaded = wasDownloaded, - contentType = contentType, - fileName = fileName, - uuid = clientUuid - ) - } - - private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? { - return thumbnail?.toLocalAttachment(this.contentType, this.fileName) - ?: if (this.contentType == null) null else PointerAttachment.forPointer(quotedAttachment = DataMessage.Quote.QuotedAttachment(contentType = this.contentType, fileName = this.fileName, thumbnail = null)).orNull() - } - private class MessageInsert( val contentValues: ContentValues, val followUp: ((Long) -> Unit)?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/InAppPaymentTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/InAppPaymentTableBackupExtensions.kt new file mode 100644 index 0000000000..9333059c38 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/InAppPaymentTableBackupExtensions.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.database + +import org.signal.core.util.deleteAll +import org.thoughtcrime.securesms.database.InAppPaymentTable + +fun InAppPaymentTable.clearAllDataForBackupRestore() { + writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableBackupExtensions.kt index 40735c4dbd..a97bf94211 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableBackupExtensions.kt @@ -16,7 +16,7 @@ import java.util.concurrent.TimeUnit private val TAG = Log.tag(MessageTable::class.java) private const val BASE_TYPE = "base_type" -fun MessageTable.getMessagesForBackup(backupTime: Long, archiveMedia: Boolean): ChatItemExportIterator { +fun MessageTable.getMessagesForBackup(backupTime: Long, mediaBackupEnabled: Boolean): ChatItemExportIterator { val cursor = readableDatabase .select( MessageTable.ID, @@ -66,7 +66,7 @@ fun MessageTable.getMessagesForBackup(backupTime: Long, archiveMedia: Boolean): .orderBy("${MessageTable.DATE_RECEIVED} ASC") .run() - return ChatItemExportIterator(cursor, 100, archiveMedia) + return ChatItemExportIterator(cursor, 100, mediaBackupEnabled) } fun MessageTable.createChatItemInserter(importState: ImportState): ChatItemImportInserter { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ReactionTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ReactionTableBackupExtensions.kt new file mode 100644 index 0000000000..28979f92f5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ReactionTableBackupExtensions.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.database + +import org.signal.core.util.deleteAll +import org.thoughtcrime.securesms.database.ReactionTable + +fun ReactionTable.clearAllDataForBackupRestore() { + writableDatabase.deleteAll(ReactionTable.TABLE_NAME) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt index 2c30843550..643e67d783 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt @@ -303,22 +303,23 @@ private fun Member.Role.toSnapshot(): Group.Member.Role { } private fun DecryptedGroup.toSnapshot(): Group.GroupSnapshot? { - if (revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION || revision == GroupsV2StateProcessor.PLACEHOLDER_REVISION) { + if (this.revision == GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION || this.revision == GroupsV2StateProcessor.PLACEHOLDER_REVISION) { return null } + return Group.GroupSnapshot( - title = Group.GroupAttributeBlob(title = title), - avatarUrl = avatar, - disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = disappearingMessagesTimer?.duration ?: 0), - accessControl = accessControl?.toSnapshot(), - version = revision, - members = members.map { it.toSnapshot() }, - membersPendingProfileKey = pendingMembers.map { it.toSnapshot() }, - membersPendingAdminApproval = requestingMembers.map { it.toSnapshot() }, - inviteLinkPassword = inviteLinkPassword, - description = Group.GroupAttributeBlob(descriptionText = description), - announcements_only = isAnnouncementGroup == EnabledState.ENABLED, - members_banned = bannedMembers.map { it.toSnapshot() } + title = Group.GroupAttributeBlob(title = this.title), + avatarUrl = this.avatar, + disappearingMessagesTimer = Group.GroupAttributeBlob(disappearingMessagesDuration = this.disappearingMessagesTimer?.duration ?: 0), + accessControl = this.accessControl?.toSnapshot(), + version = this.revision, + members = this.members.map { it.toSnapshot() }, + membersPendingProfileKey = this.pendingMembers.map { it.toSnapshot() }, + membersPendingAdminApproval = this.requestingMembers.map { it.toSnapshot() }, + inviteLinkPassword = this.inviteLinkPassword, + description = this.description.takeUnless { it.isBlank() }?.let { Group.GroupAttributeBlob(descriptionText = it) }, + announcements_only = this.isAnnouncementGroup == EnabledState.ENABLED, + members_banned = this.bannedMembers.map { it.toSnapshot() } ) } @@ -343,58 +344,58 @@ private fun Group.MemberPendingProfileKey.toLocal(operations: GroupsV2Operations private fun DecryptedPendingMember.toSnapshot(): Group.MemberPendingProfileKey { return Group.MemberPendingProfileKey( member = Group.Member( - userId = serviceIdBytes, - role = role.toSnapshot() + userId = this.serviceIdBytes, + role = this.role.toSnapshot() ), - addedByUserId = addedByAci, - timestamp = timestamp + addedByUserId = this.addedByAci, + timestamp = this.timestamp ) } private fun Group.MemberPendingAdminApproval.toLocal(): DecryptedRequestingMember { return DecryptedRequestingMember( - aciBytes = userId, - profileKey = profileKey, - timestamp = timestamp + aciBytes = this.userId, + profileKey = this.profileKey, + timestamp = this.timestamp ) } private fun DecryptedRequestingMember.toSnapshot(): Group.MemberPendingAdminApproval { return Group.MemberPendingAdminApproval( - userId = aciBytes, - profileKey = profileKey, - timestamp = timestamp + userId = this.aciBytes, + profileKey = this.profileKey, + timestamp = this.timestamp ) } private fun Group.MemberBanned.toLocal(): DecryptedBannedMember { return DecryptedBannedMember( - serviceIdBytes = userId, - timestamp = timestamp + serviceIdBytes = this.userId, + timestamp = this.timestamp ) } private fun DecryptedBannedMember.toSnapshot(): Group.MemberBanned { return Group.MemberBanned( - userId = serviceIdBytes, - timestamp = timestamp + userId = this.serviceIdBytes, + timestamp = this.timestamp ) } private fun Group.GroupSnapshot.toDecryptedGroup(operations: GroupsV2Operations.GroupOperations): DecryptedGroup { return DecryptedGroup( - title = title?.title ?: "", - avatar = avatarUrl, - disappearingMessagesTimer = DecryptedTimer(duration = disappearingMessagesTimer?.disappearingMessagesDuration ?: 0), - accessControl = accessControl?.toLocal(), - revision = version, - members = members.map { member -> member.toLocal() }, - pendingMembers = membersPendingProfileKey.map { pending -> pending.toLocal(operations) }, - requestingMembers = membersPendingAdminApproval.map { requesting -> requesting.toLocal() }, - inviteLinkPassword = inviteLinkPassword, - description = description?.descriptionText ?: "", - isAnnouncementGroup = if (announcements_only) EnabledState.ENABLED else EnabledState.DISABLED, - bannedMembers = members_banned.map { it.toLocal() } + title = this.title?.title ?: "", + avatar = this.avatarUrl, + disappearingMessagesTimer = DecryptedTimer(duration = this.disappearingMessagesTimer?.disappearingMessagesDuration ?: 0), + accessControl = this.accessControl?.toLocal(), + revision = this.version, + members = this.members.map { member -> member.toLocal() }, + pendingMembers = this.membersPendingProfileKey.map { pending -> pending.toLocal(operations) }, + requestingMembers = this.membersPendingAdminApproval.map { requesting -> requesting.toLocal() }, + inviteLinkPassword = this.inviteLinkPassword, + description = this.description?.descriptionText ?: "", + isAnnouncementGroup = if (this.announcements_only) EnabledState.ENABLED else EnabledState.DISABLED, + bannedMembers = this.members_banned.map { it.toLocal() } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatItemBackupProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatItemBackupProcessor.kt index 6860488935..36664b818a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatItemBackupProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatItemBackupProcessor.kt @@ -19,7 +19,7 @@ object ChatItemBackupProcessor { val TAG = Log.tag(ChatItemBackupProcessor::class.java) fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) { - db.messageTable.getMessagesForBackup(exportState.backupTime, exportState.allowMediaBackup).use { chatItems -> + db.messageTable.getMessagesForBackup(exportState.backupTime, exportState.mediaBackupEnabled).use { chatItems -> while (chatItems.hasNext()) { val chatItem = chatItems.next() if (chatItem != 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 18f8305364..8b8a163cab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -1855,6 +1855,7 @@ class AttachmentTable( put(ARCHIVE_THUMBNAIL_MEDIA_ID, attachment.archiveThumbnailMediaId) put(THUMBNAIL_RESTORE_STATE, ThumbnailRestoreState.NEEDS_RESTORE.value) put(ATTACHMENT_UUID, attachment.uuid?.toString()) + put(BLUR_HASH, attachment.blurHash?.hash) attachment.stickerLocator?.let { sticker -> put(STICKER_PACK_ID, sticker.packId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ChatColorsTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ChatColorsTable.kt index da522e4e0a..df754a437e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ChatColorsTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ChatColorsTable.kt @@ -13,7 +13,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore class ChatColorsTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) { companion object { - private const val TABLE_NAME = "chat_colors" + const val TABLE_NAME = "chat_colors" private const val ID = "_id" private const val CHAT_COLORS = "chat_colors" diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt index f3c5ac2ecd..9f03330124 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt @@ -51,7 +51,7 @@ import kotlin.time.Duration.Companion.seconds class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) { companion object { - private const val TABLE_NAME = "in_app_payment" + const val TABLE_NAME = "in_app_payment" /** * Row ID diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java index 0e61379133..f39e2e8a5d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java @@ -234,6 +234,10 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase } } + public void clear() { + getWritableDatabase().delete(TABLE_NAME, null, null); + } + private enum Type { BLOB(0), BOOLEAN(1), FLOAT(2), INTEGER(3), LONG(4), STRING(5); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageConverter.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageConverter.kt index 154357a19d..6808bdfde0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageConverter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageConverter.kt @@ -126,7 +126,7 @@ object GroupsV2UpdateMessageConverter { fun translateDecryptedChangeUpdate(selfIds: ServiceIds, groupContext: DecryptedGroupV2Context): GroupChangeChatUpdate { var previousGroupState = groupContext.previousGroupState val change = groupContext.change!! - if (DecryptedGroup().equals(previousGroupState)) { + if (DecryptedGroup() == previousGroupState) { previousGroupState = null } val updates: MutableList = LinkedList() diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 46a4070f9d..1e5b151c09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -433,7 +433,7 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider { BuildConfig.SIGNAL_AGENT, healthMonitor, Stories.isFeatureEnabled(), - LibSignalNetworkExtensions.createChatService(libSignalNetworkSupplier.get(), null), + LibSignalNetworkExtensions.createChatService(libSignalNetworkSupplier.get(), null, Stories.isFeatureEnabled()), shadowPercentage, bridge ); @@ -442,7 +442,7 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider { Network network = libSignalNetworkSupplier.get(); return new LibSignalChatConnection( "libsignal-unauth", - LibSignalNetworkExtensions.createChatService(network, null), + LibSignalNetworkExtensions.createChatService(network, null, Stories.isFeatureEnabled()), healthMonitor, false); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt index 9e74165ae0..92f416ddb1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ArchiveThumbnailUploadJob.kt @@ -146,10 +146,10 @@ class ArchiveThumbnailUploadJob private constructor( val uri: DecryptableUri = attachment.uri?.let { DecryptableUri(it) } ?: return null return if (MediaUtil.isImageType(attachment.contentType)) { - ImageCompressionUtil.compress(context, attachment.contentType, uri, 256, 50) + ImageCompressionUtil.compress(context, attachment.contentType ?: "", uri, 256, 50) } else if (Build.VERSION.SDK_INT >= 23 && MediaUtil.isVideoType(attachment.contentType)) { MediaUtil.getVideoThumbnail(context, attachment.uri)?.let { - ImageCompressionUtil.compress(context, attachment.contentType, uri, 256, 50) + ImageCompressionUtil.compress(context, attachment.contentType ?: "", uri, 256, 50) } } else { null diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.kt index 986e0abbb4..9ab8c67656 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SignalStore.kt @@ -288,10 +288,5 @@ class SignalStore(private val store: KeyValueStore) { instanceOverride = store _instance.reset() } - - fun clearAllDataForBackupRestore() { - releaseChannel.clearReleaseChannelRecipientId() - account.clearRegistrationButKeepCredentials() - } } } diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index aaf9e9ebc3..cb3908bae2 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -157,7 +157,7 @@ message Group { // We would use Groups.proto if we could, but we want a plaintext version to improve export readability. // For documentation, defer to Groups.proto. The only name change is Group -> GroupSnapshot to avoid the naming conflict. message GroupSnapshot { - bytes publicKey = 1; + reserved /*publicKey*/ 1; // The field is deprecated in the context of static group state GroupAttributeBlob title = 2; GroupAttributeBlob description = 11; string avatarUrl = 3; @@ -343,23 +343,46 @@ message ChatItem { } message SendStatus { - enum Status { - UNKNOWN = 0; - FAILED = 1; - PENDING = 2; - SENT = 3; - DELIVERED = 4; - READ = 5; - VIEWED = 6; - SKIPPED = 7; // e.g. user in group was blocked, so we skipped sending to them + message Pending {} + + message Sent { + bool sealedSender = 1; + } + + message Delivered { + bool sealedSender = 1; + } + + message Read { + bool sealedSender = 1; + } + + message Viewed { + bool sealedSender = 1; + } + + // e.g. user in group was blocked, so we skipped sending to them + message Skipped {} + + message Failed { + oneof reason { + bool network = 1; + bool identityKeyMismatch = 2; + } } uint64 recipientId = 1; - Status deliveryStatus = 2; - bool networkFailure = 3; - bool identityKeyMismatch = 4; - bool sealedSender = 5; - uint64 lastStatusUpdateTimestamp = 6; // the time the status was last updated -- if from a receipt, it should be the sentTime of the receipt + uint64 timestamp = 2; // the time the status was last updated -- if from a receipt, it should be the sentTime of the receipt + + oneof deliveryStatus { + Pending pending = 3; + Sent sent = 4; + Delivered delivered = 5; + Read read = 6; + Viewed viewed = 7; + Skipped skipped = 8; + Failed failed = 9; + } } message Text { @@ -565,7 +588,7 @@ message FilePointer { optional uint32 cdnNumber = 2; bytes key = 3; bytes digest = 4; - uint64 size = 5; + uint32 size = 5; // Fallback in case backup tier upload failed. optional string transitCdnKey = 6; optional uint32 transitCdnNumber = 7; @@ -652,8 +675,11 @@ message Reaction { string emoji = 1; uint64 authorId = 2; uint64 sentTimestamp = 3; + // Optional because some clients may not track this data optional uint64 receivedTimestamp = 4; - uint64 sortOrder = 5; // A higher sort order means that a reaction is more recent + // A higher sort order means that a reaction is more recent. Some clients may export this as + // incrementing numbers (e.g. 1, 2, 3), others as timestamps. + uint64 sortOrder = 5; } message ChatUpdateMessage { @@ -749,6 +775,9 @@ message SimpleChatUpdate { PAYMENT_ACTIVATION_REQUEST = 11; UNSUPPORTED_PROTOCOL_MESSAGE = 12; REPORTED_SPAM = 13; + BLOCKED = 14; + UNBLOCKED = 15; + ACCEPTED = 16; } Type type = 1; diff --git a/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt b/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt index 8ce810a62c..6a27fad417 100644 --- a/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt +++ b/core-util/src/main/java/org/signal/core/util/CursorExtensions.kt @@ -202,6 +202,15 @@ inline fun Cursor.forEach(operation: (Cursor) -> Unit) { } } +inline fun Cursor.forEachIndexed(operation: (Int, Cursor) -> Unit) { + use { + var i = 0 + while (moveToNext()) { + operation(i++, this) + } + } +} + fun Cursor.iterable(): Iterable { return CursorIterable(this) } diff --git a/dependencies.gradle.kts b/dependencies.gradle.kts index 65beb937c9..dbe38c9cc6 100644 --- a/dependencies.gradle.kts +++ b/dependencies.gradle.kts @@ -15,7 +15,7 @@ dependencyResolutionManagement { version("exoplayer", "2.19.0") version("glide", "4.15.1") version("kotlin", "1.9.20") - version("libsignal-client", "0.52.5") + version("libsignal-client", "0.54.3") version("mp4parser", "1.9.39") version("android-gradle-plugin", "8.4.0") version("accompanist", "0.28.0") diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 4fa3628980..08142a1c6c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -8760,20 +8760,20 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + + - - - + + + - - + + diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetworkExtensions.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetworkExtensions.kt index 4e73fc7228..35ddc77502 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetworkExtensions.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/websocket/LibSignalNetworkExtensions.kt @@ -16,11 +16,12 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf * Helper method to create a ChatService with optional credentials. */ fun Network.createChatService( - credentialsProvider: CredentialsProvider? = null + credentialsProvider: CredentialsProvider? = null, + receiveStories: Boolean ): ChatService { val username = credentialsProvider?.username ?: "" val password = credentialsProvider?.password ?: "" - return this.createChatService(username, password) + return this.createChatService(username, password, receiveStories) } /**