diff --git a/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt b/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt index f005e3642d..5eb0d61884 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/attachments/ArchivedAttachment.kt @@ -10,6 +10,7 @@ import android.os.Parcel import org.signal.core.util.Base64 import org.thoughtcrime.securesms.blurhash.BlurHash import org.thoughtcrime.securesms.database.AttachmentTable +import org.thoughtcrime.securesms.stickers.StickerLocator class ArchivedAttachment : Attachment { @@ -44,6 +45,7 @@ class ArchivedAttachment : Attachment { blurHash: String?, voiceNote: Boolean, borderless: Boolean, + stickerLocator: StickerLocator?, gif: Boolean, quote: Boolean ) : super( @@ -66,7 +68,7 @@ class ArchivedAttachment : Attachment { incrementalMacChunkSize = incrementalMacChunkSize ?: 0, uploadTimestamp = 0, caption = caption, - stickerLocator = null, + stickerLocator = stickerLocator, blurHash = BlurHash.parseOrNull(blurHash), audioHash = null, transformProperties = null diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 296a4f251b..781f7b3987 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.backup.v2.processor.AdHocCallBackupProcessor import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor +import org.thoughtcrime.securesms.backup.v2.processor.StickerBackupProcessor import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader @@ -129,6 +130,11 @@ object BackupRepository { eventTimer.emit("call") } + StickerBackupProcessor.export { frame -> + writer.write(frame) + eventTimer.emit("sticker-pack") + } + ChatItemBackupProcessor.export(exportState) { frame -> writer.write(frame) eventTimer.emit("message") @@ -186,6 +192,7 @@ object BackupRepository { SignalDatabase.threads.clearAllDataForBackupRestore() SignalDatabase.messages.clearAllDataForBackupRestore() SignalDatabase.attachments.clearAllDataForBackupRestore() + SignalDatabase.stickers.clearAllDataForBackupRestore() // Add back self after clearing data val selfId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(selfData.aci, selfData.pni, selfData.e164, pniVerified = true, changeSelf = true) @@ -222,6 +229,11 @@ object BackupRepository { eventTimer.emit("call") } + frame.stickerPack != null -> { + StickerBackupProcessor.import(frame.stickerPack) + eventTimer.emit("sticker-pack") + } + frame.chatItem != null -> { chatItemInserter.insert(frame.chatItem) eventTimer.emit("chatItem") diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt index 03ccb5ab5a..11ca2b4467 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ChatItemExportIterator.kt @@ -10,6 +10,7 @@ import okio.ByteString.Companion.toByteString import org.signal.core.util.Base64 import org.signal.core.util.Base64.decode import org.signal.core.util.Base64.decodeOrThrow +import org.signal.core.util.Hex import org.signal.core.util.logging.Log import org.signal.core.util.requireBlob import org.signal.core.util.requireBoolean @@ -37,6 +38,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.SendStatus import org.thoughtcrime.securesms.backup.v2.proto.SessionSwitchoverChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage +import org.thoughtcrime.securesms.backup.v2.proto.Sticker +import org.thoughtcrime.securesms.backup.v2.proto.StickerMessage import org.thoughtcrime.securesms.backup.v2.proto.Text import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate import org.thoughtcrime.securesms.database.AttachmentTable @@ -84,7 +87,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange * * All of this complexity is hidden from the user -- they just get a normal iterator interface. */ -class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator, Closeable { +class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator, Closeable { companion object { private val TAG = Log.tag(ChatItemExportIterator::class.java) @@ -104,7 +107,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: return buffer.isNotEmpty() || (cursor.count > 0 && !cursor.isLast && !cursor.isAfterLast) } - override fun next(): ChatItem { + override fun next(): ChatItem? { if (buffer.isNotEmpty()) { return buffer.remove() } @@ -344,11 +347,31 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: ) } } - record.body == null && !attachmentsById.containsKey(record.id) -> { - Log.w(TAG, "Record missing a body and doesnt have attachments, skipping") - continue + else -> { + if (record.body == null && !attachmentsById.containsKey(record.id)) { + Log.w(TAG, "Record missing a body and doesnt have attachments, skipping") + continue + } + val attachments = attachmentsById[record.id] + val sticker = attachments?.firstOrNull { dbAttachment -> + dbAttachment.isSticker + } + if (sticker != null) { + val stickerLocator = sticker.stickerLocator!! + builder.stickerMessage = StickerMessage( + sticker = Sticker( + packId = Hex.fromStringCondensed(stickerLocator.packId).toByteString(), + packKey = Hex.fromStringCondensed(stickerLocator.packKey).toByteString(), + stickerId = stickerLocator.stickerId, + emoji = stickerLocator.emoji, + data_ = sticker.toBackupAttachment().pointer + ), + reactions = reactionsById[id].toBackupReactions() + ) + } else { + builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id]) + } } - else -> builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id]) } if (record.latestRevisionId == null) { val previousEdits = revisionMap.remove(record.id) @@ -369,7 +392,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: return if (buffer.isNotEmpty()) { buffer.remove() } else { - throw NoSuchElementException() + null } } 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 474b09eb37..027969927d 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 @@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.backup.v2.database import android.content.ContentValues import androidx.core.content.contentValuesOf import org.signal.core.util.Base64 +import org.signal.core.util.Hex import org.signal.core.util.SqlUtil import org.signal.core.util.logging.Log import org.signal.core.util.orNull @@ -22,6 +23,7 @@ import org.thoughtcrime.securesms.backup.v2.BackupState 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.FilePointer import org.thoughtcrime.securesms.backup.v2.proto.GroupCall import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment @@ -31,6 +33,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.Reaction 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.database.AttachmentTable import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.GroupReceiptTable @@ -61,6 +64,7 @@ import org.thoughtcrime.securesms.payments.State import org.thoughtcrime.securesms.payments.proto.PaymentMetaData 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 @@ -340,6 +344,15 @@ class ChatItemImportInserter( } } } + if (this.stickerMessage != null) { + val sticker = this.stickerMessage.sticker + val attachment = sticker.toLocalAttachment() + if (attachment != null) { + followUp = { messageRowId -> + SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList()) + } + } + } return MessageInsert(contentValues, followUp) } @@ -804,74 +817,114 @@ class ChatItemImportInserter( } } - private fun MessageAttachment.toLocalAttachment(contentType: String? = pointer?.contentType, fileName: String? = pointer?.fileName): Attachment? { - if (pointer == null) return null - if (pointer.attachmentLocator != null) { + private fun FilePointer?.toLocalAttachment(voiceNote: Boolean, borderless: Boolean, gif: Boolean, wasDownloaded: Boolean, stickerLocator: StickerLocator? = null, contentType: String? = this?.contentType, fileName: String? = this?.fileName): Attachment? { + if (this == null) return null + + if (attachmentLocator != null) { val signalAttachmentPointer = SignalServiceAttachmentPointer( - pointer.attachmentLocator.cdnNumber, - SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey), + attachmentLocator.cdnNumber, + SignalServiceAttachmentRemoteId.from(attachmentLocator.cdnKey), contentType, - pointer.attachmentLocator.key.toByteArray(), - Optional.ofNullable(pointer.attachmentLocator.size), + attachmentLocator.key.toByteArray(), + Optional.ofNullable(attachmentLocator.size), Optional.empty(), - pointer.width ?: 0, - pointer.height ?: 0, - Optional.ofNullable(pointer.attachmentLocator.digest.toByteArray()), - Optional.ofNullable(pointer.incrementalMac?.toByteArray()), - pointer.incrementalMacChunkSize ?: 0, + width ?: 0, + height ?: 0, + Optional.ofNullable(attachmentLocator.digest.toByteArray()), + Optional.ofNullable(incrementalMac?.toByteArray()), + incrementalMacChunkSize ?: 0, Optional.ofNullable(fileName), - flag == MessageAttachment.Flag.VOICE_MESSAGE, - flag == MessageAttachment.Flag.BORDERLESS, - flag == MessageAttachment.Flag.GIF, - Optional.ofNullable(pointer.caption), - Optional.ofNullable(pointer.blurHash), - pointer.attachmentLocator.uploadTimestamp + voiceNote, + borderless, + gif, + Optional.ofNullable(caption), + Optional.ofNullable(blurHash), + attachmentLocator.uploadTimestamp ) return PointerAttachment.forPointer( pointer = Optional.of(signalAttachmentPointer), + stickerLocator = stickerLocator, transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING ).orNull() - } else if (pointer.invalidAttachmentLocator != null) { + } else if (invalidAttachmentLocator != null) { return TombstoneAttachment( contentType = contentType, - incrementalMac = pointer.incrementalMac?.toByteArray(), - incrementalMacChunkSize = pointer.incrementalMacChunkSize, - width = pointer.width, - height = pointer.height, - caption = pointer.caption, - blurHash = pointer.blurHash, - voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE, - borderless = flag == MessageAttachment.Flag.BORDERLESS, - gif = flag == MessageAttachment.Flag.GIF, + incrementalMac = incrementalMac?.toByteArray(), + incrementalMacChunkSize = incrementalMacChunkSize, + width = width, + height = height, + caption = caption, + blurHash = blurHash, + voiceNote = voiceNote, + borderless = borderless, + gif = gif, quote = false ) - } else if (pointer.backupLocator != null) { + } else if (backupLocator != null) { return ArchivedAttachment( contentType = contentType, - size = pointer.backupLocator.size.toLong(), - cdn = pointer.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber, - key = pointer.backupLocator.key.toByteArray(), - cdnKey = pointer.backupLocator.transitCdnKey, - archiveCdn = pointer.backupLocator.cdnNumber, - archiveMediaName = pointer.backupLocator.mediaName, - archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(pointer.backupLocator.mediaName)).encode(), - archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(pointer.backupLocator.mediaName)).encode(), - digest = pointer.backupLocator.digest.toByteArray(), - incrementalMac = pointer.incrementalMac?.toByteArray(), - incrementalMacChunkSize = pointer.incrementalMacChunkSize, - width = pointer.width, - height = pointer.height, - caption = pointer.caption, - blurHash = pointer.blurHash, - voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE, - borderless = flag == MessageAttachment.Flag.BORDERLESS, - gif = flag == MessageAttachment.Flag.GIF, - quote = false + size = backupLocator.size.toLong(), + cdn = backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber, + key = backupLocator.key.toByteArray(), + cdnKey = backupLocator.transitCdnKey, + archiveCdn = backupLocator.cdnNumber, + archiveMediaName = backupLocator.mediaName, + archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(backupLocator.mediaName)).encode(), + archiveThumbnailMediaId = backupState.backupKey.deriveMediaId(MediaName.forThumbnailFromMediaName(backupLocator.mediaName)).encode(), + digest = backupLocator.digest.toByteArray(), + incrementalMac = incrementalMac?.toByteArray(), + incrementalMacChunkSize = incrementalMacChunkSize, + width = width, + height = height, + caption = caption, + blurHash = blurHash, + voiceNote = voiceNote, + borderless = borderless, + gif = gif, + quote = false, + stickerLocator = stickerLocator ) } return null } + private fun Sticker?.toLocalAttachment(): Attachment? { + if (this == null) return null + + return data_.toLocalAttachment( + voiceNote = false, + gif = false, + borderless = false, + wasDownloaded = true, + stickerLocator = StickerLocator( + packId = Hex.toStringCondensed(packId.toByteArray()), + packKey = Hex.toStringCondensed(packKey.toByteArray()), + stickerId = stickerId, + emoji = emoji + ) + ) + } + + private fun MessageAttachment.toLocalAttachment(): Attachment? { + return pointer?.toLocalAttachment( + voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE, + gif = flag == MessageAttachment.Flag.GIF, + borderless = flag == MessageAttachment.Flag.BORDERLESS, + wasDownloaded = wasDownloaded + ) + } + + private fun MessageAttachment.toLocalAttachment(contentType: String?, fileName: String?): Attachment? { + return pointer?.toLocalAttachment( + voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE, + gif = flag == MessageAttachment.Flag.GIF, + borderless = flag == MessageAttachment.Flag.BORDERLESS, + wasDownloaded = wasDownloaded, + contentType = contentType, + fileName = fileName + ) + } + private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? { return thumbnail?.toLocalAttachment(this.contentType, this.fileName) ?: if (this.contentType == null) null else PointerAttachment.forPointer(quotedAttachment = DataMessage.Quote.QuotedAttachment(contentType = this.contentType, fileName = this.fileName, thumbnail = null)).orNull() diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/StickerTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/StickerTableBackupExtensions.kt new file mode 100644 index 0000000000..31f6785eff --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/StickerTableBackupExtensions.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.database + +import org.signal.core.util.deleteAll +import org.thoughtcrime.securesms.database.StickerTable + +fun StickerTable.clearAllDataForBackupRestore() { + writableDatabase.deleteAll(StickerTable.TABLE_NAME) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatItemBackupProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatItemBackupProcessor.kt index 9e998fc946..dc116d135c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatItemBackupProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/ChatItemBackupProcessor.kt @@ -20,9 +20,12 @@ object ChatItemBackupProcessor { fun export(exportState: ExportState, emitter: BackupFrameEmitter) { SignalDatabase.messages.getMessagesForBackup(exportState.backupTime, exportState.allowMediaBackup).use { chatItems -> - for (chatItem in chatItems) { - if (exportState.threadIds.contains(chatItem.chatId)) { - emitter.emit(Frame(chatItem = chatItem)) + while (chatItems.hasNext()) { + val chatItem = chatItems.next() + if (chatItem != null) { + if (exportState.threadIds.contains(chatItem.chatId)) { + emitter.emit(Frame(chatItem = chatItem)) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/StickerBackupProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/StickerBackupProcessor.kt new file mode 100644 index 0000000000..61380e24ac --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/StickerBackupProcessor.kt @@ -0,0 +1,76 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.processor + +import androidx.annotation.WorkerThread +import okio.ByteString.Companion.toByteString +import org.signal.core.util.Hex +import org.thoughtcrime.securesms.backup.v2.proto.Frame +import org.thoughtcrime.securesms.backup.v2.proto.StickerPack +import org.thoughtcrime.securesms.backup.v2.proto.StickerPackSticker +import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.database.StickerTable.StickerPackRecordReader +import org.thoughtcrime.securesms.database.StickerTable.StickerRecordReader +import org.thoughtcrime.securesms.database.model.StickerPackRecord +import org.thoughtcrime.securesms.database.model.StickerRecord +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob + +object StickerBackupProcessor { + fun export(emitter: BackupFrameEmitter) { + StickerPackRecordReader(SignalDatabase.stickers.allStickerPacks).use { reader -> + var record: StickerPackRecord? = reader.next + while (record != null) { + if (record.isInstalled) { + val frame = record.toBackupFrame() + emitter.emit(frame) + } + record = reader.next + } + } + } + + fun import(stickerPack: StickerPack) { + AppDependencies.jobManager.add( + StickerPackDownloadJob.forInstall(Hex.toStringCondensed(stickerPack.packId.toByteArray()), Hex.toStringCondensed(stickerPack.packKey.toByteArray()), false) + ) + } +} + +@WorkerThread +private fun getStickersFromDatabase(packId: String): List { + val stickers: MutableList = java.util.ArrayList() + + SignalDatabase.stickers.getStickersForPack(packId).use { cursor -> + val reader = StickerRecordReader(cursor) + var record: StickerRecord? = reader.next + while (record != null) { + stickers.add( + StickerPackSticker( + emoji = record.emoji, + id = record.stickerId + ) + ) + record = reader.next + } + } + return stickers +} + +private fun StickerPackRecord.toBackupFrame(): Frame { + val packIdBytes = Hex.fromStringCondensed(packId) + val packKey = Hex.fromStringCondensed(packKey) + val stickers = getStickersFromDatabase(packId) + val pack = StickerPack( + packId = packIdBytes.toByteString(), + packKey = packKey.toByteString(), + title = title.orElse(""), + author = author.orElse(""), + stickers = stickers + ) + return Frame(stickerPack = pack) +}