Add support for backing up wallpapers.

This commit is contained in:
Greyson Parrelli
2024-09-20 12:24:57 -04:00
committed by GitHub
parent e14078d2ec
commit a7bdfb6d76
30 changed files with 907 additions and 410 deletions

View File

@@ -6,8 +6,14 @@
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.database.AttachmentTable
fun AttachmentTable.clearAllDataForBackupRestore() {
writableDatabase.deleteAll(AttachmentTable.TABLE_NAME)
}
fun AttachmentTable.restoreWallpaperAttachment(attachment: Attachment): AttachmentId? {
return insertAttachmentsForMessage(AttachmentTable.WALLPAPER_MESSAGE_ID, listOf(attachment), emptyList()).values.firstOrNull()
}

View File

@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.content.ContentValues
import androidx.core.content.contentValuesOf
import okio.ByteString
import org.signal.core.util.Base64
import org.signal.core.util.Hex
import org.signal.core.util.SqlUtil
@@ -17,17 +16,13 @@ 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
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.LinkPreview
@@ -39,8 +34,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.MessageTable
@@ -74,9 +69,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.payments.Money
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.UuidUtil
@@ -342,7 +334,7 @@ class ChatItemImportInserter(
address.country
)
},
Contact.Avatar(null, backupContact.avatar.toLocalAttachment(), true)
Contact.Avatar(null, backupContact.avatar.toLocalAttachment(importState = importState, voiceNote = false, borderless = false, gif = false, wasDownloaded = true), true)
)
}
@@ -917,29 +909,6 @@ class ChatItemImportInserter(
?: 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)
@@ -962,6 +931,11 @@ class ChatItemImportInserter(
if (this == null) return null
return data_.toLocalAttachment(
importState = importState,
voiceNote = false,
gif = false,
borderless = false,
wasDownloaded = true,
stickerLocator = StickerLocator(
packId = Hex.toStringCondensed(packId.toByteArray()),
packKey = Hex.toStringCondensed(packKey.toByteArray()),
@@ -971,90 +945,38 @@ class ChatItemImportInserter(
)
}
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(),
iv = null,
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 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(importState = importState, voiceNote = false, borderless = false, gif = false, wasDownloaded = true))
)
}
private fun MessageAttachment.toLocalAttachment(): Attachment? {
return pointer?.toLocalAttachment(
importState = importState,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
gif = flag == MessageAttachment.Flag.GIF,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
wasDownloaded = wasDownloaded,
uuid = clientUuid
)
}
private fun MessageAttachment.toLocalAttachment(contentType: String?, fileName: String?): Attachment? {
return pointer?.toLocalAttachment(
importState = importState,
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 ContactAttachment.Name?.toLocal(): Contact.Name {

View File

@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import androidx.core.content.contentValuesOf
import com.google.protobuf.InvalidProtocolBufferException
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
@@ -16,15 +15,26 @@ import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.util.BackupConverters
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.decodeOrNull
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory
import org.thoughtcrime.securesms.wallpaper.UriChatWallpaper
import java.io.Closeable
private val TAG = Log.tag(ThreadTable::class.java)
@@ -43,14 +53,15 @@ fun ThreadTable.getThreadsForBackup(): ChatExportIterator {
${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL},
${RecipientTable.TABLE_NAME}.${RecipientTable.MENTION_SETTING},
${RecipientTable.TABLE_NAME}.${RecipientTable.CHAT_COLORS},
${RecipientTable.TABLE_NAME}.${RecipientTable.CUSTOM_CHAT_COLORS_ID}
${RecipientTable.TABLE_NAME}.${RecipientTable.CUSTOM_CHAT_COLORS_ID},
${RecipientTable.TABLE_NAME}.${RecipientTable.WALLPAPER}
FROM ${ThreadTable.TABLE_NAME}
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
WHERE ${ThreadTable.ACTIVE} = 1
"""
val cursor = readableDatabase.query(query)
return ChatExportIterator(cursor)
return ChatExportIterator(cursor, readableDatabase)
}
fun ThreadTable.clearAllDataForBackupRestore() {
@@ -59,10 +70,29 @@ fun ThreadTable.clearAllDataForBackupRestore() {
clearCache()
}
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importState: ImportState): Long? {
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importState: ImportState): Long {
val chatColor = chat.style?.toLocal(importState)
val chatColorWithId = if (chatColor != null && chatColor.id is ChatColors.Id.NotSet) {
val savedColors = SignalDatabase.chatColors.getSavedChatColors()
val match = savedColors.find { it.matchesWithoutId(chatColor) }
match ?: SignalDatabase.chatColors.saveChatColors(chatColor)
} else {
chatColor
}
// TODO [backup] Wallpaper
val wallpaperAttachmentId: AttachmentId? = chat.style?.wallpaperPhoto?.let { filePointer ->
filePointer.toLocalAttachment(importState)?.let {
SignalDatabase.attachments.restoreWallpaperAttachment(it)
}
}
val chatWallpaper = chat.style?.parseChatWallpaper(wallpaperAttachmentId)?.let {
if (chat.style.dimWallpaperInDarkMode) {
ChatWallpaperFactory.updateWithDimming(it, ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME)
} else {
it
}
}
val threadId = writableDatabase
.insertInto(ThreadTable.TABLE_NAME)
@@ -84,7 +114,9 @@ fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importSt
RecipientTable.MESSAGE_EXPIRATION_TIME to chat.expirationTimerMs,
RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION to chat.expireTimerVersion,
RecipientTable.CHAT_COLORS to chatColor?.serialize()?.encode(),
RecipientTable.CUSTOM_CHAT_COLORS_ID to (chatColor?.id ?: ChatColors.Id.NotSet).longValue
RecipientTable.CUSTOM_CHAT_COLORS_ID to (chatColor?.id ?: ChatColors.Id.NotSet).longValue,
RecipientTable.WALLPAPER_URI to if (chatWallpaper is UriChatWallpaper) chatWallpaper.uri.toString() else null,
RecipientTable.WALLPAPER to chatWallpaper?.serialize()?.encode()
),
"${RecipientTable.ID} = ?",
SqlUtil.buildArgs(recipientId.toLong())
@@ -93,7 +125,7 @@ fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importSt
return threadId
}
class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
class ChatExportIterator(private val cursor: Cursor, private val readableDatabase: SQLiteDatabase) : Iterator<Chat>, Closeable {
override fun hasNext(): Boolean {
return cursor.count > 0 && !cursor.isLast
}
@@ -103,14 +135,15 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable
throw NoSuchElementException()
}
val serializedChatColors = cursor.requireBlob(RecipientTable.CHAT_COLORS)
val chatColorId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID))
val chatColors: ChatColors? = serializedChatColors?.let { serialized ->
try {
ChatColors.forChatColor(chatColorId, ChatColor.ADAPTER.decode(serialized))
} catch (e: InvalidProtocolBufferException) {
null
}
val customChatColorsId = ChatColors.Id.forLongValue(cursor.requireLong(RecipientTable.CUSTOM_CHAT_COLORS_ID))
val chatColors: ChatColors? = cursor.requireBlob(RecipientTable.CHAT_COLORS)?.let { serializedChatColors ->
val chatColor = ChatColor.ADAPTER.decodeOrNull(serializedChatColors)
chatColor?.let { ChatColors.forChatColor(customChatColorsId, it) }
}
val chatWallpaper: Wallpaper? = cursor.requireBlob(RecipientTable.WALLPAPER)?.let { serializedWallpaper ->
Wallpaper.ADAPTER.decodeOrNull(serializedWallpaper)
}
return Chat(
@@ -123,7 +156,12 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable
muteUntilMs = cursor.requireLong(RecipientTable.MUTE_UNTIL),
markedUnread = ThreadTable.ReadStatus.deserialize(cursor.requireInt(ThreadTable.READ)) == ThreadTable.ReadStatus.FORCED_UNREAD,
dontNotifyForMentionsIfMuted = RecipientTable.MentionSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING),
style = BackupConverters.constructRemoteChatStyle(chatColors, chatColorId)
style = ChatStyleConverter.constructRemoteChatStyle(
readableDatabase = readableDatabase,
chatColors = chatColors,
chatColorId = customChatColorsId,
chatWallpaper = chatWallpaper
)
)
}
@@ -131,3 +169,15 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable
cursor.close()
}
}
private fun ChatStyle.parseChatWallpaper(wallpaperAttachmentId: AttachmentId?): ChatWallpaper? {
if (this.wallpaperPreset != null) {
return this.wallpaperPreset.toLocal()
}
if (wallpaperAttachmentId != null) {
return UriChatWallpaper(PartAuthority.getAttachmentDataUri(wallpaperAttachmentId), 0f)
}
return null
}

View File

@@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.backup.v2.util.BackupConverters
import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
@@ -50,6 +50,7 @@ object AccountDataProcessor {
val donationSubscriber = db.inAppPaymentSubscriberTable.getByCurrencyCode(donationCurrency.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
val chatColors = SignalStore.chatColors.chatColors
val chatWallpaper = SignalStore.wallpaper.currentRawWallpaper
emitter.emit(
Frame(
@@ -87,12 +88,12 @@ object AccountDataProcessor {
hasSeenGroupStoryEducationSheet = signalStore.storyValues.userHasSeenGroupStoryEducationSheet,
hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding(),
customChatColors = db.chatColorsTable.getSavedChatColors().toRemoteChatColors(),
defaultChatStyle = BackupConverters.constructRemoteChatStyle(chatColors, chatColors?.id ?: ChatColors.Id.NotSet)?.also {
it.newBuilder().apply {
// TODO [backup] We should do this elsewhere once we handle wallpaper better
dimWallpaperInDarkMode = (SignalStore.wallpaper.wallpaper?.dimLevelForDarkTheme ?: 0f) > 0f
}.build()
}
defaultChatStyle = ChatStyleConverter.constructRemoteChatStyle(
readableDatabase = db.signalReadableDatabase,
chatColors = chatColors,
chatColorId = chatColors?.id ?: ChatColors.Id.NotSet,
chatWallpaper = chatWallpaper
)
),
donationSubscriberData = donationSubscriber?.toSubscriberData(signalStore.inAppPaymentValues.isDonationSubscriptionManuallyCancelled())
)

View File

@@ -39,7 +39,7 @@ object ChatBackupProcessor {
return
}
SignalDatabase.threads.restoreFromBackup(chat, recipientId, importState)?.let { threadId ->
SignalDatabase.threads.restoreFromBackup(chat, recipientId, importState).let { threadId ->
importState.chatIdToLocalRecipientId[chat.id] = recipientId
importState.chatIdToLocalThreadId[chat.id] = threadId
importState.chatIdToBackupRecipientId[chat.id] = chat.recipientId

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.util
import okio.ByteString
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
/**
* Converts a [FilePointer] to a local [Attachment] object for inserting into the database.
*/
fun FilePointer?.toLocalAttachment(
importState: ImportState,
voiceNote: Boolean = false,
borderless: Boolean = false,
gif: Boolean = false,
wasDownloaded: Boolean = false,
stickerLocator: StickerLocator? = null,
contentType: String? = this?.contentType,
fileName: String? = this?.fileName,
uuid: ByteString? = null
): Attachment? {
if (this == null) return null
if (this.attachmentLocator != null) {
val signalAttachmentPointer = SignalServiceAttachmentPointer(
cdnNumber = this.attachmentLocator.cdnNumber,
remoteId = SignalServiceAttachmentRemoteId.from(attachmentLocator.cdnKey),
contentType = contentType,
key = this.attachmentLocator.key.toByteArray(),
size = Optional.ofNullable(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)
)
return 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) {
return 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) {
return ArchivedAttachment(
contentType = contentType,
size = this.backupLocator.size.toLong(),
cdn = this.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = this.backupLocator.key.toByteArray(),
iv = null,
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
)
}
return null
}

View File

@@ -0,0 +1,279 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.util
import android.database.Cursor
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBlob
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.mms.PartUriParser
import org.thoughtcrime.securesms.util.UriUtil
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.GradientChatWallpaper
import org.thoughtcrime.securesms.wallpaper.SingleColorChatWallpaper
/**
* Contains a collection of methods to chat styles to and from their archive format.
* These are in a file of their own just because they're rather long (with all of the various constants to map between) and used in multiple places.
*/
object ChatStyleConverter {
fun constructRemoteChatStyle(
readableDatabase: SQLiteDatabase,
chatColors: ChatColors?,
chatColorId: ChatColors.Id,
chatWallpaper: Wallpaper?
): ChatStyle? {
if (chatColors == null && chatWallpaper == null) {
return null
}
val chatStyleBuilder = ChatStyle.Builder()
if (chatColors != null) {
when (chatColorId) {
ChatColors.Id.NotSet -> {}
ChatColors.Id.Auto -> {
chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor()
}
ChatColors.Id.BuiltIn -> {
chatStyleBuilder.bubbleColorPreset = chatColors.toRemote()
}
is ChatColors.Id.Custom -> {
chatStyleBuilder.customColorId = chatColorId.longValue
}
}
}
if (chatWallpaper != null) {
when {
chatWallpaper.singleColor != null -> {
chatStyleBuilder.wallpaperPreset = chatWallpaper.singleColor.color.toRemoteWallpaperPreset()
}
chatWallpaper.linearGradient != null -> {
chatStyleBuilder.wallpaperPreset = chatWallpaper.linearGradient.toRemoteWallpaperPreset()
}
chatWallpaper.file_ != null -> {
chatStyleBuilder.wallpaperPhoto = chatWallpaper.file_.toFilePointer(readableDatabase)
}
}
chatStyleBuilder.dimWallpaperInDarkMode = chatWallpaper.dimLevelInDarkTheme > 0
}
return chatStyleBuilder.build()
}
}
fun ChatStyle.toLocal(importState: ImportState): ChatColors? {
if (this.bubbleColorPreset != null) {
return when (this.bubbleColorPreset) {
// Solids
ChatStyle.BubbleColorPreset.SOLID_CRIMSON -> ChatColorsPalette.Bubbles.CRIMSON
ChatStyle.BubbleColorPreset.SOLID_VERMILION -> ChatColorsPalette.Bubbles.VERMILION
ChatStyle.BubbleColorPreset.SOLID_BURLAP -> ChatColorsPalette.Bubbles.BURLAP
ChatStyle.BubbleColorPreset.SOLID_FOREST -> ChatColorsPalette.Bubbles.FOREST
ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN -> ChatColorsPalette.Bubbles.WINTERGREEN
ChatStyle.BubbleColorPreset.SOLID_TEAL -> ChatColorsPalette.Bubbles.TEAL
ChatStyle.BubbleColorPreset.SOLID_BLUE -> ChatColorsPalette.Bubbles.BLUE
ChatStyle.BubbleColorPreset.SOLID_INDIGO -> ChatColorsPalette.Bubbles.INDIGO
ChatStyle.BubbleColorPreset.SOLID_VIOLET -> ChatColorsPalette.Bubbles.VIOLET
ChatStyle.BubbleColorPreset.SOLID_PLUM -> ChatColorsPalette.Bubbles.PLUM
ChatStyle.BubbleColorPreset.SOLID_TAUPE -> ChatColorsPalette.Bubbles.TAUPE
ChatStyle.BubbleColorPreset.SOLID_STEEL -> ChatColorsPalette.Bubbles.STEEL
// Gradients
ChatStyle.BubbleColorPreset.GRADIENT_EMBER -> ChatColorsPalette.Bubbles.EMBER
ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT -> ChatColorsPalette.Bubbles.MIDNIGHT
ChatStyle.BubbleColorPreset.GRADIENT_INFRARED -> ChatColorsPalette.Bubbles.INFRARED
ChatStyle.BubbleColorPreset.GRADIENT_LAGOON -> ChatColorsPalette.Bubbles.LAGOON
ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT -> ChatColorsPalette.Bubbles.FLUORESCENT
ChatStyle.BubbleColorPreset.GRADIENT_BASIL -> ChatColorsPalette.Bubbles.BASIL
ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME -> ChatColorsPalette.Bubbles.SUBLIME
ChatStyle.BubbleColorPreset.GRADIENT_SEA -> ChatColorsPalette.Bubbles.SEA
ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE -> ChatColorsPalette.Bubbles.TANGERINE
ChatStyle.BubbleColorPreset.UNKNOWN_BUBBLE_COLOR_PRESET, ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE -> ChatColorsPalette.Bubbles.ULTRAMARINE
}
}
if (this.autoBubbleColor != null) {
return ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto)
}
if (this.customColorId != null) {
return importState.remoteToLocalColorId[this.customColorId]?.let { localId ->
val colorId = ChatColors.Id.forLongValue(localId)
ChatColorsPalette.Bubbles.default.withId(colorId)
}
}
return null
}
fun ChatColors.toRemote(): ChatStyle.BubbleColorPreset? {
when (this) {
// Solids
ChatColorsPalette.Bubbles.CRIMSON -> return ChatStyle.BubbleColorPreset.SOLID_CRIMSON
ChatColorsPalette.Bubbles.VERMILION -> return ChatStyle.BubbleColorPreset.SOLID_VERMILION
ChatColorsPalette.Bubbles.BURLAP -> return ChatStyle.BubbleColorPreset.SOLID_BURLAP
ChatColorsPalette.Bubbles.FOREST -> return ChatStyle.BubbleColorPreset.SOLID_FOREST
ChatColorsPalette.Bubbles.WINTERGREEN -> return ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN
ChatColorsPalette.Bubbles.TEAL -> return ChatStyle.BubbleColorPreset.SOLID_TEAL
ChatColorsPalette.Bubbles.BLUE -> return ChatStyle.BubbleColorPreset.SOLID_BLUE
ChatColorsPalette.Bubbles.INDIGO -> return ChatStyle.BubbleColorPreset.SOLID_INDIGO
ChatColorsPalette.Bubbles.VIOLET -> return ChatStyle.BubbleColorPreset.SOLID_VIOLET
ChatColorsPalette.Bubbles.PLUM -> return ChatStyle.BubbleColorPreset.SOLID_PLUM
ChatColorsPalette.Bubbles.TAUPE -> return ChatStyle.BubbleColorPreset.SOLID_TAUPE
ChatColorsPalette.Bubbles.STEEL -> return ChatStyle.BubbleColorPreset.SOLID_STEEL
ChatColorsPalette.Bubbles.ULTRAMARINE -> return ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE
// Gradients
ChatColorsPalette.Bubbles.EMBER -> return ChatStyle.BubbleColorPreset.GRADIENT_EMBER
ChatColorsPalette.Bubbles.MIDNIGHT -> return ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT
ChatColorsPalette.Bubbles.INFRARED -> return ChatStyle.BubbleColorPreset.GRADIENT_INFRARED
ChatColorsPalette.Bubbles.LAGOON -> return ChatStyle.BubbleColorPreset.GRADIENT_LAGOON
ChatColorsPalette.Bubbles.FLUORESCENT -> return ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT
ChatColorsPalette.Bubbles.BASIL -> return ChatStyle.BubbleColorPreset.GRADIENT_BASIL
ChatColorsPalette.Bubbles.SUBLIME -> return ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME
ChatColorsPalette.Bubbles.SEA -> return ChatStyle.BubbleColorPreset.GRADIENT_SEA
ChatColorsPalette.Bubbles.TANGERINE -> return ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE
}
return null
}
fun ChatStyle.WallpaperPreset.toLocal(): ChatWallpaper? {
return when (this) {
ChatStyle.WallpaperPreset.SOLID_BLUSH -> SingleColorChatWallpaper.BLUSH
ChatStyle.WallpaperPreset.SOLID_COPPER -> SingleColorChatWallpaper.COPPER
ChatStyle.WallpaperPreset.SOLID_DUST -> SingleColorChatWallpaper.DUST
ChatStyle.WallpaperPreset.SOLID_CELADON -> SingleColorChatWallpaper.CELADON
ChatStyle.WallpaperPreset.SOLID_RAINFOREST -> SingleColorChatWallpaper.RAINFOREST
ChatStyle.WallpaperPreset.SOLID_PACIFIC -> SingleColorChatWallpaper.PACIFIC
ChatStyle.WallpaperPreset.SOLID_FROST -> SingleColorChatWallpaper.FROST
ChatStyle.WallpaperPreset.SOLID_NAVY -> SingleColorChatWallpaper.NAVY
ChatStyle.WallpaperPreset.SOLID_LILAC -> SingleColorChatWallpaper.LILAC
ChatStyle.WallpaperPreset.SOLID_PINK -> SingleColorChatWallpaper.PINK
ChatStyle.WallpaperPreset.SOLID_EGGPLANT -> SingleColorChatWallpaper.EGGPLANT
ChatStyle.WallpaperPreset.SOLID_SILVER -> SingleColorChatWallpaper.SILVER
ChatStyle.WallpaperPreset.GRADIENT_SUNSET -> GradientChatWallpaper.SUNSET
ChatStyle.WallpaperPreset.GRADIENT_NOIR -> GradientChatWallpaper.NOIR
ChatStyle.WallpaperPreset.GRADIENT_HEATMAP -> GradientChatWallpaper.HEATMAP
ChatStyle.WallpaperPreset.GRADIENT_AQUA -> GradientChatWallpaper.AQUA
ChatStyle.WallpaperPreset.GRADIENT_IRIDESCENT -> GradientChatWallpaper.IRIDESCENT
ChatStyle.WallpaperPreset.GRADIENT_MONSTERA -> GradientChatWallpaper.MONSTERA
ChatStyle.WallpaperPreset.GRADIENT_BLISS -> GradientChatWallpaper.BLISS
ChatStyle.WallpaperPreset.GRADIENT_SKY -> GradientChatWallpaper.SKY
ChatStyle.WallpaperPreset.GRADIENT_PEACH -> GradientChatWallpaper.PEACH
else -> null
}
}
private fun Int.toRemoteWallpaperPreset(): ChatStyle.WallpaperPreset {
return when (this) {
SingleColorChatWallpaper.BLUSH.color -> ChatStyle.WallpaperPreset.SOLID_BLUSH
SingleColorChatWallpaper.COPPER.color -> ChatStyle.WallpaperPreset.SOLID_COPPER
SingleColorChatWallpaper.DUST.color -> ChatStyle.WallpaperPreset.SOLID_DUST
SingleColorChatWallpaper.CELADON.color -> ChatStyle.WallpaperPreset.SOLID_CELADON
SingleColorChatWallpaper.RAINFOREST.color -> ChatStyle.WallpaperPreset.SOLID_RAINFOREST
SingleColorChatWallpaper.PACIFIC.color -> ChatStyle.WallpaperPreset.SOLID_PACIFIC
SingleColorChatWallpaper.FROST.color -> ChatStyle.WallpaperPreset.SOLID_FROST
SingleColorChatWallpaper.NAVY.color -> ChatStyle.WallpaperPreset.SOLID_NAVY
SingleColorChatWallpaper.LILAC.color -> ChatStyle.WallpaperPreset.SOLID_LILAC
SingleColorChatWallpaper.PINK.color -> ChatStyle.WallpaperPreset.SOLID_PINK
SingleColorChatWallpaper.EGGPLANT.color -> ChatStyle.WallpaperPreset.SOLID_EGGPLANT
SingleColorChatWallpaper.SILVER.color -> ChatStyle.WallpaperPreset.SOLID_SILVER
else -> ChatStyle.WallpaperPreset.UNKNOWN_WALLPAPER_PRESET
}
}
private fun Wallpaper.LinearGradient.toRemoteWallpaperPreset(): ChatStyle.WallpaperPreset {
val colorArray = colors.toIntArray()
return when {
colorArray contentEquals GradientChatWallpaper.SUNSET.colors -> ChatStyle.WallpaperPreset.GRADIENT_SUNSET
colorArray contentEquals GradientChatWallpaper.NOIR.colors -> ChatStyle.WallpaperPreset.GRADIENT_NOIR
colorArray contentEquals GradientChatWallpaper.HEATMAP.colors -> ChatStyle.WallpaperPreset.GRADIENT_HEATMAP
colorArray contentEquals GradientChatWallpaper.AQUA.colors -> ChatStyle.WallpaperPreset.GRADIENT_AQUA
colorArray contentEquals GradientChatWallpaper.IRIDESCENT.colors -> ChatStyle.WallpaperPreset.GRADIENT_IRIDESCENT
colorArray contentEquals GradientChatWallpaper.MONSTERA.colors -> ChatStyle.WallpaperPreset.GRADIENT_MONSTERA
colorArray contentEquals GradientChatWallpaper.BLISS.colors -> ChatStyle.WallpaperPreset.GRADIENT_BLISS
colorArray contentEquals GradientChatWallpaper.SKY.colors -> ChatStyle.WallpaperPreset.GRADIENT_SKY
colorArray contentEquals GradientChatWallpaper.PEACH.colors -> ChatStyle.WallpaperPreset.GRADIENT_PEACH
else -> ChatStyle.WallpaperPreset.UNKNOWN_WALLPAPER_PRESET
}
}
private fun Wallpaper.File.toFilePointer(readableDatabase: SQLiteDatabase): FilePointer? {
val attachmentId: AttachmentId = UriUtil.parseOrNull(this.uri)?.let { PartUriParser(it).partId } ?: return null
val wallpaperAttachment: ArchiveAttachmentData? = readableDatabase
.select(
AttachmentTable.ARCHIVE_MEDIA_NAME,
AttachmentTable.ARCHIVE_CDN,
AttachmentTable.REMOTE_KEY,
AttachmentTable.REMOTE_DIGEST,
AttachmentTable.DATA_SIZE,
AttachmentTable.CONTENT_TYPE,
AttachmentTable.WIDTH,
AttachmentTable.HEIGHT
)
.from(AttachmentTable.TABLE_NAME)
.where("${AttachmentTable.ID} = ?", attachmentId.id)
.run()
.readToSingleObject { cursor -> cursor.toArchiveAttachmentData() }
return wallpaperAttachment?.let { attachment ->
FilePointer(
backupLocator = FilePointer.BackupLocator(
mediaName = attachment.archiveMediaName ?: "",
cdnNumber = attachment.archiveCdn,
key = attachment.remoteKey?.toByteString() ?: ByteString.EMPTY,
size = attachment.size.toInt(),
digest = attachment.remoteDigest?.toByteString() ?: ByteString.EMPTY
),
contentType = attachment.contentType,
width = attachment.width,
height = attachment.height
)
}
}
private fun Cursor.toArchiveAttachmentData(): ArchiveAttachmentData {
return ArchiveAttachmentData(
archiveMediaName = this.requireString(AttachmentTable.ARCHIVE_MEDIA_NAME),
archiveCdn = this.requireInt(AttachmentTable.ARCHIVE_CDN),
remoteKey = this.requireString(AttachmentTable.REMOTE_KEY)?.let { Base64.decodeOrNull(it) },
remoteDigest = this.requireBlob(AttachmentTable.REMOTE_DIGEST),
size = this.requireLong(AttachmentTable.DATA_SIZE),
contentType = this.requireString(AttachmentTable.CONTENT_TYPE),
width = this.requireInt(AttachmentTable.WIDTH),
height = this.requireInt(AttachmentTable.HEIGHT)
)
}
private class ArchiveAttachmentData(
val archiveMediaName: String?,
val archiveCdn: Int,
val remoteKey: ByteArray?,
val remoteDigest: ByteArray?,
val size: Long,
val contentType: String?,
val width: Int,
val height: Int
)

View File

@@ -1,112 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.util
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
// TODO [backup] Passing in chatColorId probably unnecessary. Only stored as separate column in recipient table for querying, I believe.
object BackupConverters {
fun constructRemoteChatStyle(chatColors: ChatColors?, chatColorId: ChatColors.Id): ChatStyle? {
var chatStyleBuilder: ChatStyle.Builder? = null
if (chatColors != null) {
chatStyleBuilder = ChatStyle.Builder()
when (chatColorId) {
ChatColors.Id.NotSet -> {}
ChatColors.Id.Auto -> {
chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor()
}
ChatColors.Id.BuiltIn -> {
chatStyleBuilder.bubbleColorPreset = chatColors.toRemote()
}
is ChatColors.Id.Custom -> {
chatStyleBuilder.customColorId = chatColorId.longValue
}
}
}
// TODO [backup] wallpaper
return chatStyleBuilder?.build()
}
}
fun ChatStyle.toLocal(importState: ImportState): ChatColors? {
if (this.bubbleColorPreset != null) {
return when (this.bubbleColorPreset) {
ChatStyle.BubbleColorPreset.SOLID_CRIMSON -> ChatColorsPalette.Bubbles.CRIMSON
ChatStyle.BubbleColorPreset.SOLID_VERMILION -> ChatColorsPalette.Bubbles.VERMILION
ChatStyle.BubbleColorPreset.SOLID_BURLAP -> ChatColorsPalette.Bubbles.BURLAP
ChatStyle.BubbleColorPreset.SOLID_FOREST -> ChatColorsPalette.Bubbles.FOREST
ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN -> ChatColorsPalette.Bubbles.WINTERGREEN
ChatStyle.BubbleColorPreset.SOLID_TEAL -> ChatColorsPalette.Bubbles.TEAL
ChatStyle.BubbleColorPreset.SOLID_BLUE -> ChatColorsPalette.Bubbles.BLUE
ChatStyle.BubbleColorPreset.SOLID_INDIGO -> ChatColorsPalette.Bubbles.INDIGO
ChatStyle.BubbleColorPreset.SOLID_VIOLET -> ChatColorsPalette.Bubbles.VIOLET
ChatStyle.BubbleColorPreset.SOLID_PLUM -> ChatColorsPalette.Bubbles.PLUM
ChatStyle.BubbleColorPreset.SOLID_TAUPE -> ChatColorsPalette.Bubbles.TAUPE
ChatStyle.BubbleColorPreset.SOLID_STEEL -> ChatColorsPalette.Bubbles.STEEL
ChatStyle.BubbleColorPreset.GRADIENT_EMBER -> ChatColorsPalette.Bubbles.EMBER
ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT -> ChatColorsPalette.Bubbles.MIDNIGHT
ChatStyle.BubbleColorPreset.GRADIENT_INFRARED -> ChatColorsPalette.Bubbles.INFRARED
ChatStyle.BubbleColorPreset.GRADIENT_LAGOON -> ChatColorsPalette.Bubbles.LAGOON
ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT -> ChatColorsPalette.Bubbles.FLUORESCENT
ChatStyle.BubbleColorPreset.GRADIENT_BASIL -> ChatColorsPalette.Bubbles.BASIL
ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME -> ChatColorsPalette.Bubbles.SUBLIME
ChatStyle.BubbleColorPreset.GRADIENT_SEA -> ChatColorsPalette.Bubbles.SEA
ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE -> ChatColorsPalette.Bubbles.TANGERINE
ChatStyle.BubbleColorPreset.UNKNOWN_BUBBLE_COLOR_PRESET, ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE -> ChatColorsPalette.Bubbles.ULTRAMARINE
}
}
if (this.autoBubbleColor != null) {
return ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto)
}
if (this.customColorId != null) {
return importState.remoteToLocalColorId[this.customColorId]?.let { localId ->
val colorId = ChatColors.Id.forLongValue(localId)
ChatColorsPalette.Bubbles.default.withId(colorId)
}
}
return null
}
fun ChatColors.toRemote(): ChatStyle.BubbleColorPreset? {
when (this) {
// Solids
ChatColorsPalette.Bubbles.CRIMSON -> return ChatStyle.BubbleColorPreset.SOLID_CRIMSON
ChatColorsPalette.Bubbles.VERMILION -> return ChatStyle.BubbleColorPreset.SOLID_VERMILION
ChatColorsPalette.Bubbles.BURLAP -> return ChatStyle.BubbleColorPreset.SOLID_BURLAP
ChatColorsPalette.Bubbles.FOREST -> return ChatStyle.BubbleColorPreset.SOLID_FOREST
ChatColorsPalette.Bubbles.WINTERGREEN -> return ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN
ChatColorsPalette.Bubbles.TEAL -> return ChatStyle.BubbleColorPreset.SOLID_TEAL
ChatColorsPalette.Bubbles.BLUE -> return ChatStyle.BubbleColorPreset.SOLID_BLUE
ChatColorsPalette.Bubbles.INDIGO -> return ChatStyle.BubbleColorPreset.SOLID_INDIGO
ChatColorsPalette.Bubbles.VIOLET -> return ChatStyle.BubbleColorPreset.SOLID_VIOLET
ChatColorsPalette.Bubbles.PLUM -> return ChatStyle.BubbleColorPreset.SOLID_PLUM
ChatColorsPalette.Bubbles.TAUPE -> return ChatStyle.BubbleColorPreset.SOLID_TAUPE
ChatColorsPalette.Bubbles.STEEL -> return ChatStyle.BubbleColorPreset.SOLID_STEEL
ChatColorsPalette.Bubbles.ULTRAMARINE -> return ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE
// Gradients
ChatColorsPalette.Bubbles.EMBER -> return ChatStyle.BubbleColorPreset.GRADIENT_EMBER
ChatColorsPalette.Bubbles.MIDNIGHT -> return ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT
ChatColorsPalette.Bubbles.INFRARED -> return ChatStyle.BubbleColorPreset.GRADIENT_INFRARED
ChatColorsPalette.Bubbles.LAGOON -> return ChatStyle.BubbleColorPreset.GRADIENT_LAGOON
ChatColorsPalette.Bubbles.FLUORESCENT -> return ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT
ChatColorsPalette.Bubbles.BASIL -> return ChatStyle.BubbleColorPreset.GRADIENT_BASIL
ChatColorsPalette.Bubbles.SUBLIME -> return ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME
ChatColorsPalette.Bubbles.SEA -> return ChatStyle.BubbleColorPreset.GRADIENT_SEA
ChatColorsPalette.Bubbles.TANGERINE -> return ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE
}
return null
}