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
new file mode 100644
index 0000000000..02c82903f8
--- /dev/null
+++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ImportExportTest.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2024 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.backup.v2
+
+import okio.ByteString.Companion.toByteString
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.signal.libsignal.zkgroup.profiles.ProfileKey
+import org.thoughtcrime.securesms.backup.v2.proto.AccountData
+import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
+import org.thoughtcrime.securesms.backup.v2.proto.Call
+import org.thoughtcrime.securesms.backup.v2.proto.Chat
+import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
+import org.thoughtcrime.securesms.backup.v2.proto.Frame
+import org.thoughtcrime.securesms.backup.v2.proto.Recipient
+import org.thoughtcrime.securesms.backup.v2.proto.Self
+import org.thoughtcrime.securesms.backup.v2.proto.StickerPack
+import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
+import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.whispersystems.signalservice.api.push.ServiceId
+import org.whispersystems.signalservice.api.subscriptions.SubscriberId
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.util.ArrayList
+import java.util.UUID
+import kotlin.random.Random
+
+class ImportExportTest {
+ 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 defaultBackupInfo = BackupInfo(version = 1L, backupTimeMs = 123456L)
+ val selfRecipient = Recipient(id = 1, self = Self())
+ val standardAccountData = AccountData(
+ profileKey = SELF_PROFILE_KEY.serialize().toByteString(),
+ username = "testusername",
+ usernameLink = null,
+ givenName = "Peter",
+ familyName = "Parker",
+ avatarUrlPath = "https://example.com/",
+ subscriberId = SubscriberId.generate().bytes.toByteString(),
+ subscriberCurrencyCode = "USD",
+ subscriptionManuallyCancelled = true,
+ accountSettings = AccountData.AccountSettings(
+ readReceipts = true,
+ sealedSenderIndicators = true,
+ typingIndicators = true,
+ linkPreviews = true,
+ notDiscoverableByPhoneNumber = true,
+ preferContactAvatars = true,
+ universalExpireTimer = 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")
+ )
+ )
+ }
+
+ @Before
+ fun setup() {
+ 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(
+ defaultBackupInfo,
+ standardAccountData,
+ selfRecipient
+ )
+ }
+
+ private fun importExport(vararg objects: Any) {
+ val outputStream = ByteArrayOutputStream()
+ val writer = EncryptedBackupWriter(
+ key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
+ aci = SignalStore.account().aci!!,
+ outputStream = outputStream,
+ append = { mac -> outputStream.write(mac) }
+ )
+
+ writer.use {
+ for (obj in objects) {
+ when (obj) {
+ is BackupInfo -> writer.write(obj)
+ is AccountData -> writer.write(Frame(account = obj))
+ is Recipient -> writer.write(Frame(recipient = obj))
+ is Chat -> writer.write(Frame(chat = obj))
+ is ChatItem -> writer.write(Frame(chatItem = obj))
+ is Call -> writer.write(Frame(call = obj))
+ is StickerPack -> writer.write(Frame(stickerPack = obj))
+ else -> Assert.fail("invalid object $obj")
+ }
+ }
+ }
+ val importData = outputStream.toByteArray()
+ BackupRepository.import(length = importData.size.toLong(), inputStreamFactory = { ByteArrayInputStream(importData) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
+
+ val export = BackupRepository.export()
+ compare(importData, export)
+ }
+
+ 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.call != null -> callsImported.add(f.call!!)
+ f.stickerPack != null -> stickersImported.add(f.stickerPack!!)
+ }
+ }
+
+ for (f in framesExported) {
+ when {
+ f.account != null -> accountImported.add(f.account!!)
+ f.recipient != null -> recipientsExported.add(f.recipient!!)
+ f.chat != null -> chatsExported.add(f.chat!!)
+ f.chatItem != null -> chatItemsExported.add(f.chatItem!!)
+ f.call != null -> callsExported.add(f.call!!)
+ f.stickerPack != null -> stickersExported.add(f.stickerPack!!)
+ }
+ }
+ prettyAssertEquals(accountImported, accountExported)
+ prettyAssertEquals(recipientsImported, recipientsExported) { it.id }
+ prettyAssertEquals(chatsImported, chatsExported) { it.id }
+ prettyAssertEquals(chatItemsImported, chatItemsExported)
+ prettyAssertEquals(callsImported, callsExported) { it.callId }
+ prettyAssertEquals(stickersImported, stickersExported) { it.packId }
+ }
+
+ private 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 $a1 \n $a2")
+ }
+ }
+ }
+
+ private fun > prettyAssertEquals(import: List, export: List, selector: (T) -> R?) {
+ 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,
+ streamLength = import.size.toLong(),
+ dataStream = inputFactory
+ )
+ val frames = ArrayList()
+ while (frameReader.hasNext()) {
+ frames.add(frameReader.next())
+ }
+
+ return frames
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt
index afa86aa8f2..d5e85d13e2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt
@@ -28,6 +28,7 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import org.whispersystems.signalservice.api.storage.StorageRecordProtoUtil.defaultAccountRecord
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.api.util.UuidUtil
+import kotlin.jvm.optionals.getOrNull
object AccountDataProcessor {
@@ -47,12 +48,11 @@ object AccountDataProcessor {
familyName = self.profileName.familyName,
avatarUrlPath = self.profileAvatar ?: "",
subscriptionManuallyCancelled = SignalStore.donationsValues().isUserManuallyCancelled(),
- username = SignalStore.account().username,
+ username = self.username.getOrNull(),
subscriberId = subscriber?.subscriberId?.bytes?.toByteString() ?: defaultAccountRecord.subscriberId,
subscriberCurrencyCode = subscriber?.currencyCode ?: defaultAccountRecord.subscriberCurrencyCode,
accountSettings = AccountData.AccountSettings(
storyViewReceiptsEnabled = SignalStore.storyValues().viewedReceiptsEnabled,
- noteToSelfMarkedUnread = record != null && record.syncExtras.isForcedUnread,
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context),
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
@@ -61,13 +61,14 @@ object AccountDataProcessor {
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
preferContactAvatars = SignalStore.settings().isPreferSystemContactPhotos,
universalExpireTimer = SignalStore.settings().universalExpireTimer,
- preferredReactionEmoji = SignalStore.emojiValues().reactions,
+ preferredReactionEmoji = SignalStore.emojiValues().rawReactions,
storiesDisabled = SignalStore.storyValues().isFeatureDisabled,
hasViewedOnboardingStory = SignalStore.storyValues().userHasViewedOnboardingStory,
hasSetMyStoriesPrivacy = SignalStore.storyValues().userHasBeenNotifiedAboutStories,
keepMutedChatsArchived = SignalStore.settings().shouldKeepMutedChatsArchived(),
displayBadgesOnProfile = SignalStore.donationsValues().getDisplayBadgesOnProfile(),
- hasSeenGroupStoryEducationSheet = SignalStore.storyValues().userHasSeenGroupStoryEducationSheet
+ hasSeenGroupStoryEducationSheet = SignalStore.storyValues().userHasSeenGroupStoryEducationSheet,
+ hasCompletedUsernameOnboarding = SignalStore.uiHints().hasCompletedUsernameOnboarding()
)
)
)
@@ -122,6 +123,14 @@ object AccountDataProcessor {
)
SignalStore.misc().usernameQrCodeColorScheme = accountData.usernameLink.color.toLocalUsernameColor()
}
+
+ if (settings.preferredReactionEmoji.isNotEmpty()) {
+ SignalStore.emojiValues().reactions = settings.preferredReactionEmoji
+ }
+
+ if (settings.hasCompletedUsernameOnboarding) {
+ SignalStore.uiHints().setHasCompletedUsernameOnboarding(true)
+ }
}
SignalDatabase.runPostSuccessfulTransaction { ProfileUtil.handleSelfProfileKeyChange() }