diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/WallpaperAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/WallpaperAttachment.kt new file mode 100644 index 0000000000..06877ef329 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/WallpaperAttachment.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.attachments + +import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties +import org.thoughtcrime.securesms.util.MediaUtil + +/** + * A basically-empty [Attachment] that is solely used for inserting an attachment into the [AttachmentTable]. + */ +class WallpaperAttachment() : Attachment( + contentType = MediaUtil.IMAGE_WEBP, + transferState = AttachmentTable.TRANSFER_PROGRESS_DONE, + size = 0, + fileName = null, + cdn = Cdn.CDN_0, + remoteLocation = null, + remoteKey = null, + remoteIv = null, + remoteDigest = null, + incrementalDigest = null, + fastPreflightId = null, + voiceNote = false, + borderless = false, + videoGif = false, + width = 0, + height = 0, + incrementalMacChunkSize = 0, + quote = false, + uploadTimestamp = 0, + caption = null, + stickerLocator = null, + blurHash = null, + audioHash = null, + transformProperties = TransformProperties.empty(), + uuid = null +) { + override val uri = null + override val publicUri = null + override val thumbnailUri = null +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/AttachmentTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/AttachmentTableBackupExtensions.kt index 79986f7c1f..3f62008b3e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/AttachmentTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/AttachmentTableBackupExtensions.kt @@ -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() +} 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 ab71d2dc1e..55324d3bf0 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 @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableBackupExtensions.kt index c2f4036617..638b2e1b36 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableBackupExtensions.kt @@ -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, Closeable { +class ChatExportIterator(private val cursor: Cursor, private val readableDatabase: SQLiteDatabase) : Iterator, Closeable { override fun hasNext(): Boolean { return cursor.count > 0 && !cursor.isLast } @@ -103,14 +135,15 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator, 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, 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, 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 +} 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 5dd3c31e89..0ab62eec08 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 @@ -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()) ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatBackupProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatBackupProcessor.kt index c3d8492b25..4ff4d2fe9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatBackupProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatBackupProcessor.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveProtoExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveProtoExtensions.kt new file mode 100644 index 0000000000..c26d17f0c8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ArchiveProtoExtensions.kt @@ -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 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ChatStyleConverter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ChatStyleConverter.kt new file mode 100644 index 0000000000..309af8e92d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ChatStyleConverter.kt @@ -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 +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ModelConverters.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ModelConverters.kt deleted file mode 100644 index ced7dc638b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ModelConverters.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ChatColors.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ChatColors.kt index fc4fa18f95..aa564b8df0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ChatColors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/ChatColors.kt @@ -33,6 +33,7 @@ class ChatColors( ) : Parcelable { fun isGradient(): Boolean = linearGradient != null + fun isSolid(): Boolean = singleColor != null /** * Returns the Drawable to render the linear gradient, or null if this ChatColors is a single color. 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 680cf60469..23fe99ccd4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -70,6 +70,7 @@ import org.thoughtcrime.securesms.attachments.Attachment import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.attachments.Cdn import org.thoughtcrime.securesms.attachments.DatabaseAttachment +import org.thoughtcrime.securesms.attachments.WallpaperAttachment import org.thoughtcrime.securesms.audio.AudioHash import org.thoughtcrime.securesms.blurhash.BlurHash import org.thoughtcrime.securesms.crypto.AttachmentSecret @@ -182,6 +183,7 @@ class AttachmentTable( const val TRANSFER_RESTORE_IN_PROGRESS = 6 const val TRANSFER_RESTORE_OFFLOADED = 7 const val PREUPLOAD_MESSAGE_ID: Long = -8675309 + const val WALLPAPER_MESSAGE_ID: Long = -8675308 private val PROJECTION = arrayOf( ID, @@ -816,7 +818,7 @@ class AttachmentTable( fun trimAllAbandonedAttachments() { val deleteCount = writableDatabase .delete(TABLE_NAME) - .where("$MESSAGE_ID != $PREUPLOAD_MESSAGE_ID AND $MESSAGE_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})") + .where("$MESSAGE_ID != $PREUPLOAD_MESSAGE_ID AND $MESSAGE_ID != $WALLPAPER_MESSAGE_ID AND $MESSAGE_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})") .run() if (deleteCount > 0) { @@ -2184,6 +2186,16 @@ class AttachmentTable( throw MmsException(e) } + return insertAttachmentWithData(messageId, dataStream, attachment, quote) + } + + /** + * Inserts an attachment with existing data. This is likely an outgoing attachment that we're in the process of sending. + * + * @param dataStream The stream to read the data from. This stream will be closed by this method. + */ + @Throws(MmsException::class) + private fun insertAttachmentWithData(messageId: Long, dataStream: InputStream, attachment: Attachment, quote: Boolean): AttachmentId { // To avoid performing long-running operations in a transaction, we write the data to an independent file first in a way that doesn't rely on db state. val fileWriteResult: DataFileWriteResult = writeToDataFile(newDataFile(context), dataStream, attachment.transformProperties ?: TransformProperties.empty()) Log.d(TAG, "[insertAttachmentWithData] Wrote data to file: ${fileWriteResult.file.absolutePath} (MessageId: $messageId, ${attachment.uri})") @@ -2212,12 +2224,16 @@ class AttachmentTable( } if (hashMatch != null) { - if (fileWriteResult.hash == hashMatch.hashStart) { - Log.i(TAG, "[insertAttachmentWithData] Found that the new attachment hash matches the DATA_HASH_START of ${hashMatch.id}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})") - } else if (fileWriteResult.hash == hashMatch.hashEnd) { - Log.i(TAG, "[insertAttachmentWithData] Found that the new attachment hash matches the DATA_HASH_END of ${hashMatch.id}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})") - } else { - throw IllegalStateException("Should not be possible based on query.") + when (fileWriteResult.hash) { + hashMatch.hashStart -> { + Log.i(TAG, "[insertAttachmentWithData] Found that the new attachment hash matches the DATA_HASH_START of ${hashMatch.id}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})") + } + hashMatch.hashEnd -> { + Log.i(TAG, "[insertAttachmentWithData] Found that the new attachment hash matches the DATA_HASH_END of ${hashMatch.id}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})") + } + else -> { + throw IllegalStateException("Should not be possible based on query.") + } } contentValues.put(DATA_FILE, hashMatch.file.absolutePath) @@ -2309,6 +2325,21 @@ class AttachmentTable( return attachmentId } + fun insertWallpaper(dataStream: InputStream): AttachmentId { + return insertAttachmentWithData(WALLPAPER_MESSAGE_ID, dataStream, WallpaperAttachment(), quote = false).also { id -> + createKeyIvIfNecessary(id) + } + } + + fun getAllWallpapers(): List { + return readableDatabase + .select(ID) + .from(TABLE_NAME) + .where("$MESSAGE_ID = $WALLPAPER_MESSAGE_ID") + .run() + .readToList { AttachmentId(it.requireLong(ID)) } + } + private fun getTransferFile(db: SQLiteDatabase, attachmentId: AttachmentId): File? { return db .select(TRANSFER_FILE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 1728d1b0c2..593a4b219e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.util.ProfileUtil import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.wallpaper.ChatWallpaper +import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory import org.thoughtcrime.securesms.wallpaper.WallpaperStorage import org.whispersystems.signalservice.api.profiles.SignalServiceProfile import org.whispersystems.signalservice.api.push.ServiceId @@ -1968,7 +1969,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da for (pair in idWithWallpaper) { AppDependencies.databaseObserver.notifyRecipientChanged(pair.first) if (pair.second != null) { - WallpaperStorage.onWallpaperDeselected(context, Uri.parse(pair.second)) + WallpaperStorage.onWallpaperDeselected(Uri.parse(pair.second)) } } } else { @@ -1980,11 +1981,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } } - fun setWallpaper(id: RecipientId, chatWallpaper: ChatWallpaper?) { - setWallpaper(id, chatWallpaper?.serialize()) + fun setWallpaper(id: RecipientId, chatWallpaper: ChatWallpaper?, notifyDeselected: Boolean) { + setWallpaper(id, chatWallpaper?.serialize(), notifyDeselected) } - private fun setWallpaper(id: RecipientId, wallpaper: Wallpaper?) { + private fun setWallpaper(id: RecipientId, wallpaper: Wallpaper?, notifyDeselected: Boolean) { val existingWallpaperUri = getWallpaperUri(id) val values = ContentValues().apply { put(WALLPAPER, wallpaper?.encode()) @@ -1999,8 +2000,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da AppDependencies.databaseObserver.notifyRecipientChanged(id) } - if (existingWallpaperUri != null) { - WallpaperStorage.onWallpaperDeselected(context, existingWallpaperUri) + if (notifyDeselected && existingWallpaperUri != null) { + WallpaperStorage.onWallpaperDeselected(existingWallpaperUri) } } @@ -2010,7 +2011,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da .dimLevelInDarkTheme(if (enabled) ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME else 0f) .build() - setWallpaper(id, updated) + setWallpaper(id, updated, false) } private fun getWallpaper(id: RecipientId): Wallpaper? { @@ -2055,6 +2056,23 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return 0 } + /** + * Migrates all recipients using [legacyUri] for their wallpaper to [newUri]. + * Needed for an app migration. + */ + fun migrateWallpaperUri(legacyUri: Uri, newUri: Uri): Int { + val newWallpaper = ChatWallpaperFactory.create(newUri) + + return writableDatabase + .update(TABLE_NAME) + .values( + WALLPAPER to newWallpaper.serialize().encode(), + WALLPAPER_URI to newUri.toString() + ) + .where("$WALLPAPER_URI = ?", legacyUri) + .run() + } + fun getPhoneNumberDiscoverability(id: RecipientId): PhoneNumberDiscoverableState? { return readableDatabase .select(PHONE_NUMBER_DISCOVERABLE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreMediaJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreMediaJob.kt index 40dae504cf..c4735ceb23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreMediaJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupRestoreMediaJob.kt @@ -66,9 +66,11 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo val messageMap = SignalDatabase.messages.getMessages(messageIds).associate { it.id to (it as MmsMessageRecord) } for (attachment in attachmentBatch) { + val isWallpaper = attachment.mmsId == AttachmentTable.WALLPAPER_MESSAGE_ID + val message = messageMap[attachment.mmsId] - if (message == null) { - Log.w(TAG, "Unable to find message for ${attachment.attachmentId}") + if (message == null && !isWallpaper) { + Log.w(TAG, "Unable to find message for ${attachment.attachmentId}, mmsId: ${attachment.mmsId}") notRestorable += attachment continue } @@ -79,7 +81,7 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo highPriority = false ) - if (shouldRestoreFullSize(message, restoreTime, SignalStore.backup.optimizeStorage)) { + if (isWallpaper || shouldRestoreFullSize(message!!, restoreTime, SignalStore.backup.optimizeStorage)) { restoreFullAttachmentJobs += attachment to RestoreAttachmentJob( messageId = attachment.mmsId, attachmentId = attachment.attachmentId diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index cd807ca86a..58a17a1e4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -94,6 +94,7 @@ import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob; import org.thoughtcrime.securesms.migrations.UpdateSmsJobsMigrationJob; import org.thoughtcrime.securesms.migrations.UserNotificationMigrationJob; import org.thoughtcrime.securesms.migrations.UuidMigrationJob; +import org.thoughtcrime.securesms.migrations.WallpaperStorageMigrationJob; import java.util.Arrays; import java.util.HashMap; @@ -309,6 +310,7 @@ public final class JobManagerFactories { put(UpdateSmsJobsMigrationJob.KEY, new UpdateSmsJobsMigrationJob.Factory()); put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory()); put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory()); + put(WallpaperStorageMigrationJob.KEY, new WallpaperStorageMigrationJob.Factory()); // Dead jobs put(FailingJob.KEY, new FailingJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/WallpaperValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/WallpaperValues.java index f5b14c6287..1501fa0195 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/WallpaperValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/WallpaperValues.java @@ -1,6 +1,5 @@ package org.thoughtcrime.securesms.keyvalue; -import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; @@ -35,8 +34,8 @@ public final class WallpaperValues extends SignalStoreValues { return Collections.emptyList(); } - public void setWallpaper(@NonNull Context context, @Nullable ChatWallpaper wallpaper) { - Wallpaper currentWallpaper = getCurrentWallpaper(); + public void setWallpaper(@Nullable ChatWallpaper wallpaper) { + Wallpaper currentWallpaper = getCurrentRawWallpaper(); Uri currentUri = null; if (currentWallpaper != null && currentWallpaper.file_ != null) { @@ -50,12 +49,12 @@ public final class WallpaperValues extends SignalStoreValues { } if (currentUri != null) { - WallpaperStorage.onWallpaperDeselected(context, currentUri); + WallpaperStorage.onWallpaperDeselected(currentUri); } } public @Nullable ChatWallpaper getWallpaper() { - Wallpaper currentWallpaper = getCurrentWallpaper(); + Wallpaper currentWallpaper = getCurrentRawWallpaper(); if (currentWallpaper != null) { return ChatWallpaperFactory.create(currentWallpaper); @@ -69,7 +68,7 @@ public final class WallpaperValues extends SignalStoreValues { } public void setDimInDarkTheme(boolean enabled) { - Wallpaper currentWallpaper = getCurrentWallpaper(); + Wallpaper currentWallpaper = getCurrentRawWallpaper(); if (currentWallpaper != null) { putBlob(KEY_WALLPAPER, @@ -88,7 +87,7 @@ public final class WallpaperValues extends SignalStoreValues { * wallpaper is both set *and* it's an image. */ public @Nullable Uri getWallpaperUri() { - Wallpaper currentWallpaper = getCurrentWallpaper(); + Wallpaper currentWallpaper = getCurrentRawWallpaper(); if (currentWallpaper != null && currentWallpaper.file_ != null) { return Uri.parse(currentWallpaper.file_.uri); @@ -97,7 +96,10 @@ public final class WallpaperValues extends SignalStoreValues { } } - private @Nullable Wallpaper getCurrentWallpaper() { + /** + * Allows for retrieval of the raw, serialized wallpaper proto. Clients should prefer {@link #getWallpaper()} instead. + */ + public @Nullable Wallpaper getCurrentRawWallpaper() { byte[] serialized = getBlob(KEY_WALLPAPER, null); if (serialized != null) { @@ -111,4 +113,12 @@ public final class WallpaperValues extends SignalStoreValues { return null; } } + + /** + * For a migration, we need to update the current wallpaper _without_ triggering the onDeselectedEvents and such. + * For normal usage, use {@link #setWallpaper(ChatWallpaper)} + */ + public void setRawWallpaperForMigration(@NonNull Wallpaper wallpaper) { + putBlob(KEY_WALLPAPER, wallpaper.encode()); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 645e0edbc9..89935c2845 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -156,9 +156,10 @@ public class ApplicationMigrations { // static final int BACKFILL_DIGESTS = 112; static final int BACKFILL_DIGESTS_V2 = 113; static final int CALL_LINK_STORAGE_SYNC = 114; + static final int WALLPAPER_MIGRATION = 115; } - public static final int CURRENT_VERSION = 114; + public static final int CURRENT_VERSION = 115; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -713,6 +714,10 @@ public class ApplicationMigrations { jobs.put(Version.CALL_LINK_STORAGE_SYNC, new SyncCallLinksMigrationJob()); } + if (lastSeenVersion < Version.WALLPAPER_MIGRATION) { + jobs.put(Version.WALLPAPER_MIGRATION, new WallpaperStorageMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/WallpaperStorageMigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/WallpaperStorageMigrationJob.kt new file mode 100644 index 0000000000..3971e57046 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/WallpaperStorageMigrationJob.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.migrations + +import android.content.Context +import android.net.Uri +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.mms.PartAuthority +import org.thoughtcrime.securesms.util.storage.FileStorage +import java.io.File +import java.io.IOException + +/** + * We need to move the wallpapers to be stored in the attachment table as part of backups V2. + */ +internal class WallpaperStorageMigrationJob(parameters: Parameters = Parameters.Builder().build()) : MigrationJob(parameters) { + companion object { + private val TAG = Log.tag(WallpaperStorageMigrationJob::class.java) + const val KEY = "WallpaperStorageMigrationJob" + + private const val DIRECTORY = "wallpapers" + private const val FILENAME_BASE = "wallpaper" + + private val CONTENT_URI = Uri.parse("content://${BuildConfig.APPLICATION_ID}/wallpaper") + } + + override fun getFactoryKey(): String = KEY + + override fun isUiBlocking(): Boolean = true + + override fun performMigration() { + val wallpaperFileNames = FileStorage.getAll(context, DIRECTORY, FILENAME_BASE) + + if (wallpaperFileNames.isEmpty()) { + Log.i(TAG, "No wallpapers to migrate. Done.") + return + } + + Log.i(TAG, "There are ${wallpaperFileNames.size} wallpapers to migrate.") + + val currentDefaultWallpaperUri = SignalStore.wallpaper.currentRawWallpaper?.file_?.uri + + for (filename in wallpaperFileNames) { + val inputStream = FileStorage.read(context, DIRECTORY, filename) + val wallpaperAttachmentId = SignalDatabase.attachments.insertWallpaper(inputStream) + + val directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE) + val file = File(directory, filename) + + val legacyUri = Uri.withAppendedPath(CONTENT_URI, filename) + val newUri = PartAuthority.getAttachmentDataUri(wallpaperAttachmentId) + + val updatedUserCount = SignalDatabase.recipients.migrateWallpaperUri( + legacyUri = legacyUri, + newUri = newUri + ) + Log.d(TAG, "Wallpaper with name '$filename' was in use by $updatedUserCount recipients.") + + if (currentDefaultWallpaperUri == legacyUri.toString()) { + Log.d(TAG, "Wallpaper with name '$filename' was set as the default wallpaper. Updating.") + SignalStore.wallpaper.setRawWallpaperForMigration(Wallpaper(file_ = Wallpaper.File(uri = newUri.toString()))) + } + + val deleted = file.delete() + if (!deleted) { + Log.w(TAG, "Failed to delete wallpaper file: $file") + } + } + + Log.i(TAG, "Successfully migrated ${wallpaperFileNames.size} wallpapers.") + } + + override fun shouldRetry(e: Exception): Boolean = e is IOException + + class Factory : Job.Factory { + override fun create(parameters: Parameters, serializedData: ByteArray?): WallpaperStorageMigrationJob { + return WallpaperStorageMigrationJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java index 70ac37e718..87b549a31f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -34,13 +34,11 @@ public class PartAuthority { private static final String PART_URI_STRING = "content://" + AUTHORITY + "/part"; private static final String PART_THUMBNAIL_STRING = "content://" + AUTHORITY + "/thumbnail"; private static final String STICKER_URI_STRING = "content://" + AUTHORITY + "/sticker"; - private static final String WALLPAPER_URI_STRING = "content://" + AUTHORITY + "/wallpaper"; private static final String EMOJI_URI_STRING = "content://" + AUTHORITY + "/emoji"; private static final String AVATAR_PICKER_URI_STRING = "content://" + AUTHORITY + "/avatar_picker"; private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING); private static final Uri PART_THUMBNAIL_URI = Uri.parse(PART_THUMBNAIL_STRING); private static final Uri STICKER_CONTENT_URI = Uri.parse(STICKER_URI_STRING); - private static final Uri WALLPAPER_CONTENT_URI = Uri.parse(WALLPAPER_URI_STRING); private static final Uri EMOJI_CONTENT_URI = Uri.parse(EMOJI_URI_STRING); private static final Uri AVATAR_PICKER_CONTENT_URI = Uri.parse(AVATAR_PICKER_URI_STRING); @@ -84,7 +82,6 @@ public class PartAuthority { case STICKER_ROW: return SignalDatabase.stickers().getStickerStream(ContentUris.parseId(uri)); case PERSISTENT_ROW: return DeprecatedPersistentBlobProvider.getInstance(context).getStream(context, ContentUris.parseId(uri)); case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri); - case WALLPAPER_ROW: return WallpaperStorage.read(context, getWallpaperFilename(uri)); case EMOJI_ROW: return EmojiFiles.openForReading(context, getEmojiFilename(uri)); case AVATAR_PICKER_ROW: return AvatarPickerStorage.read(context, getAvatarPickerFilename(uri)); case THUMBNAIL_ROW: return SignalDatabase.attachments().getAttachmentThumbnailStream(new PartUriParser(uri).getPartId(), 0); @@ -190,10 +187,6 @@ public class PartAuthority { return ContentUris.withAppendedId(STICKER_CONTENT_URI, id); } - public static Uri getWallpaperUri(String filename) { - return Uri.withAppendedPath(WALLPAPER_CONTENT_URI, filename); - } - public static Uri getAvatarPickerUri(String filename) { return Uri.withAppendedPath(AVATAR_PICKER_CONTENT_URI, filename); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ProtoExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ProtoExtensions.kt new file mode 100644 index 0000000000..8a2a76ced4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ProtoExtensions.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.util + +import com.google.protobuf.InvalidProtocolBufferException +import com.squareup.wire.ProtoAdapter + +/** + * Performs the common pattern of attempting to decode a serialized proto and returning null if it fails to decode. + */ +fun ProtoAdapter.decodeOrNull(serialized: ByteArray): E? { + return try { + this.decode(serialized) + } catch (e: InvalidProtocolBufferException) { + null + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UriUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/UriUtil.java deleted file mode 100644 index 051b3e4287..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/UriUtil.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; - -import androidx.annotation.NonNull; - -import java.io.File; -import java.io.IOException; - -public final class UriUtil { - - /** - * Ensures that an external URI is valid and doesn't contain any references to internal files or - * any other trickiness. - */ - public static boolean isValidExternalUri(@NonNull Context context, @NonNull Uri uri) { - if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - try { - File file = new File(uri.getPath()); - - return file.getCanonicalPath().equals(file.getPath()) && - !file.getCanonicalPath().startsWith("/data") && - !file.getCanonicalPath().contains(context.getPackageName()); - } catch (IOException e) { - return false; - } - } else { - return true; - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/UriUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/UriUtil.kt new file mode 100644 index 0000000000..b63769f46e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/UriUtil.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.util + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import java.io.File +import java.io.IOException + +object UriUtil { + + /** + * Ensures that an external URI is valid and doesn't contain any references to internal files or + * any other trickiness. + */ + @JvmStatic + fun isValidExternalUri(context: Context, uri: Uri): Boolean { + if (ContentResolver.SCHEME_FILE == uri.scheme) { + try { + val file = File(uri.path) + + return file.canonicalPath == file.path && + !file.canonicalPath.startsWith("/data") && + !file.canonicalPath.contains(context.packageName) + } catch (e: IOException) { + return false + } + } else { + return true + } + } + + /** + * Parses a string to a URI if it's valid, otherwise null. + */ + fun parseOrNull(uri: String): Uri? { + return try { + Uri.parse(uri) + } catch (e: Exception) { + null + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperRepository.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperRepository.java index 5aed75ac27..972b53b74d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/ChatWallpaperRepository.java @@ -9,7 +9,6 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette; import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -50,7 +49,7 @@ class ChatWallpaperRepository { EXECUTOR.execute(() -> { List wallpapers = new ArrayList<>(ChatWallpaper.BuiltIns.INSTANCE.getAllBuiltIns()); - wallpapers.addAll(WallpaperStorage.getAll(AppDependencies.getApplication())); + wallpapers.addAll(WallpaperStorage.getAll()); consumer.accept(wallpapers); }); } @@ -59,17 +58,17 @@ class ChatWallpaperRepository { if (recipientId != null) { //noinspection CodeBlock2Expr EXECUTOR.execute(() -> { - SignalDatabase.recipients().setWallpaper(recipientId, chatWallpaper); + SignalDatabase.recipients().setWallpaper(recipientId, chatWallpaper, true); onWallpaperSaved.run(); }); } else { - SignalStore.wallpaper().setWallpaper(AppDependencies.getApplication(), chatWallpaper); + SignalStore.wallpaper().setWallpaper(chatWallpaper); onWallpaperSaved.run(); } } void resetAllWallpaper(@NonNull Runnable onWallpaperReset) { - SignalStore.wallpaper().setWallpaper(AppDependencies.getApplication(), null); + SignalStore.wallpaper().setWallpaper(null); EXECUTOR.execute(() -> { SignalDatabase.recipients().resetAllWallpaper(); onWallpaperReset.run(); @@ -95,7 +94,8 @@ class ChatWallpaperRepository { .setWallpaper(recipientId, ChatWallpaperFactory.updateWithDimming(recipient.getWallpaper(), dimInDarkTheme ? ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME - : 0f)); + : 0f), + false); } else { throw new IllegalStateException("Unexpected call to setDimInDarkTheme, no wallpaper has been set on the given recipient or globally."); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/GradientChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/GradientChatWallpaper.java index e9f1659008..028c967267 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/GradientChatWallpaper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/GradientChatWallpaper.java @@ -17,42 +17,42 @@ import java.util.Objects; public final class GradientChatWallpaper implements ChatWallpaper, Parcelable { - public static final ChatWallpaper SUNSET = new GradientChatWallpaper(168f, - new int[] { 0xFFF3DC47, 0xFFF3DA47, 0xFFF2D546, 0xFFF2CC46, 0xFFF1C146, 0xFFEFB445, 0xFFEEA544, 0xFFEC9644, 0xFFEB8743, 0xFFE97743, 0xFFE86942, 0xFFE65C41, 0xFFE55041, 0xFFE54841, 0xFFE44240, 0xFFE44040 }, - new float[] { 0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1f }, - 0f); - public static final ChatWallpaper NOIR = new GradientChatWallpaper(180f, - new int[] { 0xFF16161D, 0xFF17171E, 0xFF1A1A22, 0xFF1F1F28, 0xFF26262F, 0xFF2D2D38, 0xFF353542, 0xFF3E3E4C, 0xFF474757, 0xFF4F4F61, 0xFF57576B, 0xFF5F5F74, 0xFF65657C, 0xFF6A6A82, 0xFF6D6D85, 0xFF6E6E87 }, - new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, - 0f); - public static final ChatWallpaper HEATMAP = new GradientChatWallpaper(192f, - new int[] { 0xFFF53844, 0xFFF33845, 0xFFEC3848, 0xFFE2384C, 0xFFD63851, 0xFFC73857, 0xFFB6385E, 0xFFA43866, 0xFF93376D, 0xFF813775, 0xFF70377C, 0xFF613782, 0xFF553787, 0xFF4B378B, 0xFF44378E, 0xFF42378F }, - new float[] { 0.0000f, 0.0075f, 0.0292f, 0.0637f, 0.1097f, 0.1659f, 0.2310f, 0.3037f, 0.3827f, 0.4666f, 0.5541f, 0.6439f, 0.7347f, 0.8252f, 0.9141f, 1.0000f }, - 0f); - public static final ChatWallpaper AQUA = new GradientChatWallpaper(180f, - new int[] { 0xFF0093E9, 0xFF0294E9, 0xFF0696E7, 0xFF0D99E5, 0xFF169EE3, 0xFF21A3E0, 0xFF2DA8DD, 0xFF3AAEDA, 0xFF46B5D6, 0xFF53BBD3, 0xFF5FC0D0, 0xFF6AC5CD, 0xFF73CACB, 0xFF7ACDC9, 0xFF7ECFC7, 0xFF80D0C7 }, - new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, - 0f); - public static final ChatWallpaper IRIDESCENT = new GradientChatWallpaper(192f, - new int[] { 0xFFF04CE6, 0xFFEE4BE6, 0xFFE54AE5, 0xFFD949E5, 0xFFC946E4, 0xFFB644E3, 0xFFA141E3, 0xFF8B3FE2, 0xFF743CE1, 0xFF5E39E0, 0xFF4936DF, 0xFF3634DE, 0xFF2632DD, 0xFF1930DD, 0xFF112FDD, 0xFF0E2FDD }, - new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, - 0f); - public static final ChatWallpaper MONSTERA = new GradientChatWallpaper(180f, - new int[] { 0xFF65CDAC, 0xFF64CDAB, 0xFF60CBA8, 0xFF5BC8A3, 0xFF55C49D, 0xFF4DC096, 0xFF45BB8F, 0xFF3CB687, 0xFF33B17F, 0xFF2AAC76, 0xFF21A76F, 0xFF1AA268, 0xFF139F62, 0xFF0E9C5E, 0xFF0B9A5B, 0xFF0A995A }, - new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, - 0f); - public static final ChatWallpaper BLISS = new GradientChatWallpaper(180f, - new int[] { 0xFFD8E1FA, 0xFFD8E0F9, 0xFFD8DEF7, 0xFFD8DBF3, 0xFFD8D6EE, 0xFFD7D1E8, 0xFFD7CCE2, 0xFFD7C6DB, 0xFFD7BFD4, 0xFFD7B9CD, 0xFFD6B4C7, 0xFFD6AFC1, 0xFFD6AABC, 0xFFD6A7B8, 0xFFD6A5B6, 0xFFD6A4B5 }, - new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, - 0f); - public static final ChatWallpaper SKY = new GradientChatWallpaper(180f, - new int[] { 0xFFD8EBFD, 0xFFD7EAFD, 0xFFD5E9FD, 0xFFD2E7FD, 0xFFCDE5FD, 0xFFC8E3FD, 0xFFC3E0FD, 0xFFBDDDFC, 0xFFB7DAFC, 0xFFB2D7FC, 0xFFACD4FC, 0xFFA7D1FC, 0xFFA3CFFB, 0xFFA0CDFB, 0xFF9ECCFB, 0xFF9DCCFB }, - new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, - 0f); - public static final ChatWallpaper PEACH = new GradientChatWallpaper(192f, - new int[] { 0xFFFFE5C2, 0xFFFFE4C1, 0xFFFFE2BF, 0xFFFFDFBD, 0xFFFEDBB9, 0xFFFED6B5, 0xFFFED1B1, 0xFFFDCCAC, 0xFFFDC6A8, 0xFFFDC0A3, 0xFFFCBB9F, 0xFFFCB69B, 0xFFFCB297, 0xFFFCAF95, 0xFFFCAD93, 0xFFFCAC92 }, - new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, - 0f); + public static final GradientChatWallpaper SUNSET = new GradientChatWallpaper(168f, + new int[] { 0xFFF3DC47, 0xFFF3DA47, 0xFFF2D546, 0xFFF2CC46, 0xFFF1C146, 0xFFEFB445, 0xFFEEA544, 0xFFEC9644, 0xFFEB8743, 0xFFE97743, 0xFFE86942, 0xFFE65C41, 0xFFE55041, 0xFFE54841, 0xFFE44240, 0xFFE44040 }, + new float[] { 0.0f, 0.0807f, 0.1554f, 0.225f, 0.2904f, 0.3526f, 0.4125f, 0.471f, 0.529f, 0.5875f, 0.6474f, 0.7096f, 0.775f, 0.8446f, 0.9193f, 1f }, + 0f); + public static final GradientChatWallpaper NOIR = new GradientChatWallpaper(180f, + new int[] { 0xFF16161D, 0xFF17171E, 0xFF1A1A22, 0xFF1F1F28, 0xFF26262F, 0xFF2D2D38, 0xFF353542, 0xFF3E3E4C, 0xFF474757, 0xFF4F4F61, 0xFF57576B, 0xFF5F5F74, 0xFF65657C, 0xFF6A6A82, 0xFF6D6D85, 0xFF6E6E87 }, + new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, + 0f); + public static final GradientChatWallpaper HEATMAP = new GradientChatWallpaper(192f, + new int[] { 0xFFF53844, 0xFFF33845, 0xFFEC3848, 0xFFE2384C, 0xFFD63851, 0xFFC73857, 0xFFB6385E, 0xFFA43866, 0xFF93376D, 0xFF813775, 0xFF70377C, 0xFF613782, 0xFF553787, 0xFF4B378B, 0xFF44378E, 0xFF42378F }, + new float[] { 0.0000f, 0.0075f, 0.0292f, 0.0637f, 0.1097f, 0.1659f, 0.2310f, 0.3037f, 0.3827f, 0.4666f, 0.5541f, 0.6439f, 0.7347f, 0.8252f, 0.9141f, 1.0000f }, + 0f); + public static final GradientChatWallpaper AQUA = new GradientChatWallpaper(180f, + new int[] { 0xFF0093E9, 0xFF0294E9, 0xFF0696E7, 0xFF0D99E5, 0xFF169EE3, 0xFF21A3E0, 0xFF2DA8DD, 0xFF3AAEDA, 0xFF46B5D6, 0xFF53BBD3, 0xFF5FC0D0, 0xFF6AC5CD, 0xFF73CACB, 0xFF7ACDC9, 0xFF7ECFC7, 0xFF80D0C7 }, + new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, + 0f); + public static final GradientChatWallpaper IRIDESCENT = new GradientChatWallpaper(192f, + new int[] { 0xFFF04CE6, 0xFFEE4BE6, 0xFFE54AE5, 0xFFD949E5, 0xFFC946E4, 0xFFB644E3, 0xFFA141E3, 0xFF8B3FE2, 0xFF743CE1, 0xFF5E39E0, 0xFF4936DF, 0xFF3634DE, 0xFF2632DD, 0xFF1930DD, 0xFF112FDD, 0xFF0E2FDD }, + new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, + 0f); + public static final GradientChatWallpaper MONSTERA = new GradientChatWallpaper(180f, + new int[] { 0xFF65CDAC, 0xFF64CDAB, 0xFF60CBA8, 0xFF5BC8A3, 0xFF55C49D, 0xFF4DC096, 0xFF45BB8F, 0xFF3CB687, 0xFF33B17F, 0xFF2AAC76, 0xFF21A76F, 0xFF1AA268, 0xFF139F62, 0xFF0E9C5E, 0xFF0B9A5B, 0xFF0A995A }, + new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, + 0f); + public static final GradientChatWallpaper BLISS = new GradientChatWallpaper(180f, + new int[] { 0xFFD8E1FA, 0xFFD8E0F9, 0xFFD8DEF7, 0xFFD8DBF3, 0xFFD8D6EE, 0xFFD7D1E8, 0xFFD7CCE2, 0xFFD7C6DB, 0xFFD7BFD4, 0xFFD7B9CD, 0xFFD6B4C7, 0xFFD6AFC1, 0xFFD6AABC, 0xFFD6A7B8, 0xFFD6A5B6, 0xFFD6A4B5 }, + new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, + 0f); + public static final GradientChatWallpaper SKY = new GradientChatWallpaper(180f, + new int[] { 0xFFD8EBFD, 0xFFD7EAFD, 0xFFD5E9FD, 0xFFD2E7FD, 0xFFCDE5FD, 0xFFC8E3FD, 0xFFC3E0FD, 0xFFBDDDFC, 0xFFB7DAFC, 0xFFB2D7FC, 0xFFACD4FC, 0xFFA7D1FC, 0xFFA3CFFB, 0xFFA0CDFB, 0xFF9ECCFB, 0xFF9DCCFB }, + new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, + 0f); + public static final GradientChatWallpaper PEACH = new GradientChatWallpaper(192f, + new int[] { 0xFFFFE5C2, 0xFFFFE4C1, 0xFFFFE2BF, 0xFFFFDFBD, 0xFFFEDBB9, 0xFFFED6B5, 0xFFFED1B1, 0xFFFDCCAC, 0xFFFDC6A8, 0xFFFDC0A3, 0xFFFCBB9F, 0xFFFCB69B, 0xFFFCB297, 0xFFFCAF95, 0xFFFCAD93, 0xFFFCAC92 }, + new float[] { 0.0000f, 0.0807f, 0.1554f, 0.2250f, 0.2904f, 0.3526f, 0.4125f, 0.4710f, 0.5290f, 0.5875f, 0.6474f, 0.7096f, 0.7750f, 0.8446f, 0.9193f, 1.0000f }, + 0f); private final float degrees; @@ -155,6 +155,10 @@ public final class GradientChatWallpaper implements ChatWallpaper, Parcelable { return result; } + public int[] getColors() { + return colors; + } + public static final Creator CREATOR = new Creator() { @Override public GradientChatWallpaper createFromParcel(Parcel in) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/SingleColorChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/SingleColorChatWallpaper.java index 442ee16ae6..bcbf8b281a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/SingleColorChatWallpaper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/SingleColorChatWallpaper.java @@ -14,18 +14,18 @@ import java.util.Objects; public final class SingleColorChatWallpaper implements ChatWallpaper, Parcelable { - public static final ChatWallpaper BLUSH = new SingleColorChatWallpaper(0xFFE26983, 0f); - public static final ChatWallpaper COPPER = new SingleColorChatWallpaper(0xFFDF9171, 0f); - public static final ChatWallpaper DUST = new SingleColorChatWallpaper(0xFF9E9887, 0f); - public static final ChatWallpaper CELADON = new SingleColorChatWallpaper(0xFF89AE8F, 0f); - public static final ChatWallpaper RAINFOREST = new SingleColorChatWallpaper(0xFF146148, 0f); - public static final ChatWallpaper PACIFIC = new SingleColorChatWallpaper(0xFF32C7E2, 0f); - public static final ChatWallpaper FROST = new SingleColorChatWallpaper(0xFF7C99B6, 0f); - public static final ChatWallpaper NAVY = new SingleColorChatWallpaper(0xFF403B91, 0f); - public static final ChatWallpaper LILAC = new SingleColorChatWallpaper(0xFFC988E7, 0f); - public static final ChatWallpaper PINK = new SingleColorChatWallpaper(0xFFE297C3, 0f); - public static final ChatWallpaper EGGPLANT = new SingleColorChatWallpaper(0xFF624249, 0f); - public static final ChatWallpaper SILVER = new SingleColorChatWallpaper(0xFFA2A2AA, 0f); + public static final SingleColorChatWallpaper BLUSH = new SingleColorChatWallpaper(0xFFE26983, 0f); + public static final SingleColorChatWallpaper COPPER = new SingleColorChatWallpaper(0xFFDF9171, 0f); + public static final SingleColorChatWallpaper DUST = new SingleColorChatWallpaper(0xFF9E9887, 0f); + public static final SingleColorChatWallpaper CELADON = new SingleColorChatWallpaper(0xFF89AE8F, 0f); + public static final SingleColorChatWallpaper RAINFOREST = new SingleColorChatWallpaper(0xFF146148, 0f); + public static final SingleColorChatWallpaper PACIFIC = new SingleColorChatWallpaper(0xFF32C7E2, 0f); + public static final SingleColorChatWallpaper FROST = new SingleColorChatWallpaper(0xFF7C99B6, 0f); + public static final SingleColorChatWallpaper NAVY = new SingleColorChatWallpaper(0xFF403B91, 0f); + public static final SingleColorChatWallpaper LILAC = new SingleColorChatWallpaper(0xFFC988E7, 0f); + public static final SingleColorChatWallpaper PINK = new SingleColorChatWallpaper(0xFFE297C3, 0f); + public static final SingleColorChatWallpaper EGGPLANT = new SingleColorChatWallpaper(0xFF624249, 0f); + public static final SingleColorChatWallpaper SILVER = new SingleColorChatWallpaper(0xFFA2A2AA, 0f); private final @ColorInt int color; private final float dimLevelInDarkTheme; @@ -95,6 +95,10 @@ public final class SingleColorChatWallpaper implements ChatWallpaper, Parcelable return Objects.hash(color, dimLevelInDarkTheme); } + public int getColor() { + return color; + } + public static final Creator CREATOR = new Creator() { @Override public SingleColorChatWallpaper createFromParcel(Parcel in) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java index fcd3b32abd..2fbf65da84 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/UriChatWallpaper.java @@ -27,7 +27,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -final class UriChatWallpaper implements ChatWallpaper, Parcelable { +public final class UriChatWallpaper implements ChatWallpaper, Parcelable { private static final LruCache CACHE = new LruCache((int) Runtime.getRuntime().maxMemory() / 8) { @Override @@ -118,6 +118,10 @@ final class UriChatWallpaper implements ChatWallpaper, Parcelable { return false; } + public @NonNull Uri getUri() { + return uri; + } + @Override public @NonNull Wallpaper serialize() { return new Wallpaper.Builder() diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/WallpaperStorage.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/WallpaperStorage.java index 1064da017e..12197f34ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/WallpaperStorage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/WallpaperStorage.java @@ -1,18 +1,19 @@ package org.thoughtcrime.securesms.wallpaper; -import android.content.Context; import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.database.SignalDatabase; +import org.thoughtcrime.securesms.dependencies.AppDependencies; +import org.thoughtcrime.securesms.jobs.UploadAttachmentToArchiveJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.mms.PartAuthority; -import org.thoughtcrime.securesms.util.storage.FileStorage; +import org.thoughtcrime.securesms.mms.PartUriParser; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -26,31 +27,28 @@ public final class WallpaperStorage { private static final String TAG = Log.tag(WallpaperStorage.class); - private static final String DIRECTORY = "wallpapers"; - private static final String FILENAME_BASE = "wallpaper"; - /** * Saves the provided input stream as a new wallpaper file. */ @WorkerThread - public static @NonNull ChatWallpaper save(@NonNull Context context, @NonNull InputStream wallpaperStream, @NonNull String extension) throws IOException { - String name = FileStorage.save(context, wallpaperStream, DIRECTORY, FILENAME_BASE, extension); + public static @NonNull ChatWallpaper save(@NonNull InputStream wallpaperStream) throws IOException { + AttachmentId attachmentId = SignalDatabase.attachments().insertWallpaper(wallpaperStream); - return ChatWallpaperFactory.create(PartAuthority.getWallpaperUri(name)); + if (SignalStore.backup().backsUpMedia()) { + AppDependencies.getJobManager().add(new UploadAttachmentToArchiveJob(attachmentId)); + } + + return ChatWallpaperFactory.create(PartAuthority.getAttachmentDataUri(attachmentId)); } @WorkerThread - public static @NonNull InputStream read(@NonNull Context context, String filename) throws IOException { - return FileStorage.read(context, DIRECTORY, filename); - } - - @WorkerThread - public static @NonNull List getAll(@NonNull Context context) { - return FileStorage.getAll(context, DIRECTORY, FILENAME_BASE) - .stream() - .map(PartAuthority::getWallpaperUri) - .map(ChatWallpaperFactory::create) - .collect(Collectors.toList()); + public static @NonNull List getAll() { + return SignalDatabase.attachments() + .getAllWallpapers() + .stream() + .map(PartAuthority::getAttachmentDataUri) + .map(ChatWallpaperFactory::create) + .collect(Collectors.toList()); } /** @@ -58,7 +56,7 @@ public final class WallpaperStorage { * if we discover it's unused, we'll delete the file. */ @WorkerThread - public static void onWallpaperDeselected(@NonNull Context context, @NonNull Uri uri) { + public static void onWallpaperDeselected(@NonNull Uri uri) { Uri globalUri = SignalStore.wallpaper().getWallpaperUri(); if (Objects.equals(uri, globalUri)) { return; @@ -69,12 +67,7 @@ public final class WallpaperStorage { return; } - String filename = PartAuthority.getWallpaperFilename(uri); - File directory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); - File wallpaperFile = new File(directory, filename); - - if (!wallpaperFile.delete()) { - Log.w(TAG, "Failed to delete " + filename + "!"); - } + AttachmentId attachmentId = new PartUriParser(uri).getPartId(); + SignalDatabase.attachments().deleteAttachment(attachmentId); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropRepository.java b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropRepository.java index 9b8e81a947..0eaf5b92cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/wallpaper/crop/WallpaperCropRepository.java @@ -33,14 +33,14 @@ final class WallpaperCropRepository { @WorkerThread @NonNull ChatWallpaper setWallPaper(byte[] bytes) throws IOException { try (InputStream inputStream = new ByteArrayInputStream(bytes)) { - ChatWallpaper wallpaper = WallpaperStorage.save(context, inputStream, "webp"); + ChatWallpaper wallpaper = WallpaperStorage.save(inputStream); if (recipientId != null) { Log.i(TAG, "Setting image wallpaper for " + recipientId); - SignalDatabase.recipients().setWallpaper(recipientId, wallpaper); + SignalDatabase.recipients().setWallpaper(recipientId, wallpaper, true); } else { Log.i(TAG, "Setting image wallpaper for default"); - SignalStore.wallpaper().setWallpaper(context, wallpaper); + SignalStore.wallpaper().setWallpaper(wallpaper); } return wallpaper; diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index b0b63315c8..aaa1aab0dd 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -621,7 +621,7 @@ message FilePointer { oneof locator { BackupLocator backupLocator = 1; - AttachmentLocator attachmentLocator= 2; + AttachmentLocator attachmentLocator = 2; InvalidAttachmentLocator invalidAttachmentLocator = 3; } diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/UriUtilTest_isValidExternalUri.java b/app/src/test/java/org/thoughtcrime/securesms/util/UriUtilTest_isValidExternalUri.java index b7d83448fb..1d499cb7d2 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/util/UriUtilTest_isValidExternalUri.java +++ b/app/src/test/java/org/thoughtcrime/securesms/util/UriUtilTest_isValidExternalUri.java @@ -1,3 +1,8 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.thoughtcrime.securesms.util; import android.app.Application; @@ -10,7 +15,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.ParameterizedRobolectricTestRunner; import org.robolectric.annotation.Config; -import org.thoughtcrime.securesms.BuildConfig; import java.util.Arrays; import java.util.Collection; @@ -24,7 +28,7 @@ public class UriUtilTest_isValidExternalUri { private final String input; private final boolean output; - private static final String APPLICATION_ID = BuildConfig.APPLICATION_ID; + private static final String APPLICATION_ID = "org.thoughtcrime.securesms"; @ParameterizedRobolectricTestRunner.Parameters public static Collection data() { diff --git a/libsignal-service/build.gradle.kts b/libsignal-service/build.gradle.kts index fb5370fd9c..98c1c4c7df 100644 --- a/libsignal-service/build.gradle.kts +++ b/libsignal-service/build.gradle.kts @@ -39,7 +39,8 @@ tasks.withType().configureEach { afterEvaluate { listOf( "runKtlintCheckOverMainSourceSet", - "runKtlintFormatOverMainSourceSet" + "runKtlintFormatOverMainSourceSet", + "sourcesJar" ).forEach { taskName -> tasks.named(taskName) { mustRunAfter(tasks.named("generateMainProtos"))