Add sticker specific restore flow and fix archive related sticker bugs.

This commit is contained in:
Cody Henthorne
2025-08-27 09:39:12 -04:00
committed by Michelle Tang
parent 9903a664d4
commit 21363f085e
15 changed files with 738 additions and 70 deletions

View File

@@ -67,6 +67,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.LocalStickerAttachment
import org.thoughtcrime.securesms.attachments.WallpaperAttachment
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.backup.v2.exporters.ChatItemArchiveExporter
@@ -532,7 +533,7 @@ class AttachmentTable(
fun getLast30DaysOfRestorableAttachments(batchSize: Int): List<RestorableAttachment> {
val thirtyDaysAgo = System.currentTimeMillis().milliseconds - 30.days
return readableDatabase
.select("$TABLE_NAME.$ID", MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY)
.select("$TABLE_NAME.$ID", MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY, STICKER_PACK_ID)
.from("$TABLE_NAME INNER JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $TABLE_NAME.$MESSAGE_ID")
.where("$TRANSFER_STATE = ? AND ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} >= ?", TRANSFER_NEEDS_RESTORE, thirtyDaysAgo.inWholeMilliseconds)
.limit(batchSize)
@@ -544,7 +545,8 @@ class AttachmentTable(
mmsId = it.requireLong(MESSAGE_ID),
size = it.requireLong(DATA_SIZE),
plaintextHash = it.requireString(DATA_HASH_END)?.let { hash -> Base64.decode(hash) },
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) }
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) },
stickerPackId = it.requireString(STICKER_PACK_ID)
)
}
}
@@ -556,7 +558,7 @@ class AttachmentTable(
fun getOlderRestorableAttachments(batchSize: Int): List<RestorableAttachment> {
val thirtyDaysAgo = System.currentTimeMillis().milliseconds - 30.days
return readableDatabase
.select("$TABLE_NAME.$ID", MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY)
.select("$TABLE_NAME.$ID", MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY, STICKER_PACK_ID)
.from("$TABLE_NAME INNER JOIN ${MessageTable.TABLE_NAME} ON ${MessageTable.TABLE_NAME}.${MessageTable.ID} = $TABLE_NAME.$MESSAGE_ID")
.where("$TRANSFER_STATE = ? AND ${MessageTable.TABLE_NAME}.${MessageTable.DATE_RECEIVED} < ?", TRANSFER_NEEDS_RESTORE, thirtyDaysAgo.inWholeMilliseconds)
.limit(batchSize)
@@ -568,14 +570,15 @@ class AttachmentTable(
mmsId = it.requireLong(MESSAGE_ID),
size = it.requireLong(DATA_SIZE),
plaintextHash = it.requireString(DATA_HASH_END)?.let { hash -> Base64.decode(hash) },
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) }
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) },
stickerPackId = it.requireString(STICKER_PACK_ID)
)
}
}
fun getRestorableAttachments(batchSize: Int): List<RestorableAttachment> {
return readableDatabase
.select(ID, MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY)
.select(ID, MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY, STICKER_PACK_ID)
.from(TABLE_NAME)
.where("$TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE)
.limit(batchSize)
@@ -587,14 +590,15 @@ class AttachmentTable(
mmsId = it.requireLong(MESSAGE_ID),
size = it.requireLong(DATA_SIZE),
plaintextHash = it.requireString(DATA_HASH_END)?.let { hash -> Base64.decode(hash) },
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) }
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) },
stickerPackId = it.requireString(STICKER_PACK_ID)
)
}
}
fun getRestorableOptimizedAttachments(): List<RestorableAttachment> {
return readableDatabase
.select(ID, MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY)
.select(ID, MESSAGE_ID, DATA_SIZE, DATA_HASH_END, REMOTE_KEY, STICKER_PACK_ID)
.from(TABLE_NAME)
.where("$TRANSFER_STATE = ? AND $DATA_HASH_END NOT NULL AND $REMOTE_KEY NOT NULL", TRANSFER_RESTORE_OFFLOADED)
.orderBy("$ID DESC")
@@ -605,7 +609,8 @@ class AttachmentTable(
mmsId = it.requireLong(MESSAGE_ID),
size = it.requireLong(DATA_SIZE),
plaintextHash = it.requireString(DATA_HASH_END)?.let { hash -> Base64.decode(hash) },
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) }
remoteKey = it.requireString(REMOTE_KEY)?.let { key -> Base64.decode(key) },
stickerPackId = it.requireString(STICKER_PACK_ID)
)
}
}
@@ -1760,6 +1765,7 @@ class AttachmentTable(
val insertedAttachments: MutableMap<Attachment, AttachmentId> = mutableMapOf()
for (attachment in attachments) {
val attachmentId = when {
attachment is LocalStickerAttachment -> insertLocalStickerAttachment(mmsId, attachment)
attachment.uri != null -> insertAttachmentWithData(mmsId, attachment, attachment.quote)
attachment is ArchivedAttachment -> insertArchivedAttachment(mmsId, attachment, attachment.quote)
else -> insertUndownloadedAttachment(mmsId, attachment, attachment.quote)
@@ -2460,8 +2466,103 @@ class AttachmentTable(
}
/**
* Inserts an attachment with existing data. This is likely an outgoing attachment that we're in the process of sending or
* an incoming sticker we have already downloaded.
* Inserts an incoming sticker with pre-existing local data (i.e., the sticker pack is installed).
*/
@Throws(MmsException::class)
private fun insertLocalStickerAttachment(messageId: Long, stickerAttachment: LocalStickerAttachment): AttachmentId {
Log.d(TAG, "[insertLocalStickerAttachment] Inserting attachment for messageId $messageId. (MessageId: $messageId, ${stickerAttachment.uri})")
// find sticker record and reuse
var attachmentId: AttachmentId? = null
writableDatabase.withinTransaction { db ->
val match = db.select()
.from(TABLE_NAME)
.where("$DATA_FILE NOT NULL AND $DATA_RANDOM NOT NULL AND $STICKER_PACK_ID = ? AND $STICKER_ID = ?", stickerAttachment.packId, stickerAttachment.stickerId)
.run()
.readToSingleObject {
it.readAttachment() to it.readDataFileInfo()!!
}
if (match != null) {
val (attachment, dataFileInfo) = match
Log.i(TAG, "[insertLocalStickerAttachment] Found that the sticker matches an existing sticker attachment: ${attachment.attachmentId}. Using all of it's fields. (MessageId: $messageId, ${attachment.uri})")
val contentValues = ContentValues().apply {
put(MESSAGE_ID, messageId)
put(CONTENT_TYPE, attachment.contentType)
put(REMOTE_KEY, attachment.remoteKey)
put(REMOTE_LOCATION, attachment.remoteLocation)
put(REMOTE_DIGEST, attachment.remoteDigest)
put(CDN_NUMBER, attachment.cdn.serialize())
put(TRANSFER_STATE, attachment.transferState)
put(DATA_FILE, dataFileInfo.file.absolutePath)
put(DATA_SIZE, attachment.size)
put(DATA_RANDOM, dataFileInfo.random)
put(FAST_PREFLIGHT_ID, stickerAttachment.fastPreflightId)
put(WIDTH, attachment.width)
put(HEIGHT, attachment.height)
put(STICKER_PACK_ID, attachment.stickerLocator!!.packId)
put(STICKER_PACK_KEY, attachment.stickerLocator.packKey)
put(STICKER_ID, attachment.stickerLocator.stickerId)
put(STICKER_EMOJI, attachment.stickerLocator.emoji)
put(BLUR_HASH, attachment.blurHash?.hash)
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
put(DATA_HASH_START, dataFileInfo.hashStart)
put(DATA_HASH_END, dataFileInfo.hashEnd ?: dataFileInfo.hashStart)
put(ARCHIVE_CDN, attachment.archiveCdn)
put(ARCHIVE_TRANSFER_STATE, attachment.archiveTransferState.value)
put(THUMBNAIL_RESTORE_STATE, dataFileInfo.thumbnailRestoreState)
put(THUMBNAIL_RANDOM, dataFileInfo.thumbnailRandom)
put(THUMBNAIL_FILE, dataFileInfo.thumbnailFile)
put(ATTACHMENT_UUID, stickerAttachment.uuid?.toString())
}
val rowId = db.insert(TABLE_NAME, null, contentValues)
attachmentId = AttachmentId(rowId)
}
}
if (attachmentId == null) {
val dataStream = try {
PartAuthority.getAttachmentStream(context, stickerAttachment.uri)
} catch (e: IOException) {
throw MmsException(e)
}
val fileWriteResult: DataFileWriteResult = writeToDataFile(newDataFile(context), dataStream, stickerAttachment.transformProperties ?: TransformProperties.empty())
Log.d(TAG, "[insertLocalStickerAttachment] Wrote data to file: ${fileWriteResult.file.absolutePath} (MessageId: $messageId, ${stickerAttachment.uri})")
val remoteKey = Util.getSecretBytes(64)
val contentValues = ContentValues().apply {
put(MESSAGE_ID, messageId)
put(CONTENT_TYPE, stickerAttachment.contentType)
put(REMOTE_KEY, Base64.encodeWithPadding(remoteKey))
put(TRANSFER_STATE, stickerAttachment.transferState)
put(DATA_FILE, fileWriteResult.file.absolutePath)
put(DATA_SIZE, fileWriteResult.length)
put(DATA_RANDOM, fileWriteResult.random)
put(FAST_PREFLIGHT_ID, stickerAttachment.fastPreflightId)
put(WIDTH, stickerAttachment.width)
put(HEIGHT, stickerAttachment.height)
put(STICKER_PACK_ID, stickerAttachment.stickerLocator!!.packId)
put(STICKER_PACK_KEY, stickerAttachment.stickerLocator.packKey)
put(STICKER_ID, stickerAttachment.stickerLocator.stickerId)
put(STICKER_EMOJI, stickerAttachment.stickerLocator.emoji)
put(DATA_HASH_START, fileWriteResult.hash)
put(DATA_HASH_END, fileWriteResult.hash)
put(ATTACHMENT_UUID, stickerAttachment.uuid?.toString())
}
val rowId = writableDatabase.insert(TABLE_NAME, null, contentValues)
attachmentId = AttachmentId(rowId)
}
return attachmentId
}
/**
* Inserts an attachment with existing data. This is likely an outgoing attachment that we're in the process of sending.
*/
@Throws(MmsException::class)
private fun insertAttachmentWithData(messageId: Long, attachment: Attachment, quote: Boolean): AttachmentId {
@@ -3138,7 +3239,8 @@ class AttachmentTable(
val mmsId: Long,
val size: Long,
val plaintextHash: ByteArray?,
val remoteKey: ByteArray?
val remoteKey: ByteArray?,
val stickerPackId: String?
) {
override fun equals(other: Any?): Boolean {
return this === other || attachmentId == (other as? RestorableAttachment)?.attachmentId

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.database
import android.content.Context
import android.database.Cursor
import androidx.core.content.contentValuesOf
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.StreamUtil
import org.signal.core.util.delete
@@ -98,23 +99,37 @@ class StickerTable(
fun insertSticker(sticker: IncomingSticker, dataStream: InputStream, notify: Boolean) {
val fileInfo: FileInfo = saveStickerImage(dataStream)
writableDatabase
.insertInto(TABLE_NAME)
.values(
PACK_ID to sticker.packId,
PACK_KEY to sticker.packKey,
PACK_TITLE to sticker.packTitle,
PACK_AUTHOR to sticker.packAuthor,
STICKER_ID to sticker.stickerId,
EMOJI to sticker.emoji,
CONTENT_TYPE to sticker.contentType,
COVER to if (sticker.isCover) 1 else 0,
INSTALLED to if (sticker.isInstalled) 1 else 0,
FILE_PATH to fileInfo.file.absolutePath,
FILE_LENGTH to fileInfo.length,
FILE_RANDOM to fileInfo.random
)
.run(SQLiteDatabase.CONFLICT_REPLACE)
val values = contentValuesOf(
PACK_ID to sticker.packId,
PACK_KEY to sticker.packKey,
PACK_TITLE to sticker.packTitle,
PACK_AUTHOR to sticker.packAuthor,
STICKER_ID to sticker.stickerId,
EMOJI to sticker.emoji,
CONTENT_TYPE to sticker.contentType,
COVER to if (sticker.isCover) 1 else 0,
INSTALLED to if (sticker.isInstalled) 1 else 0,
FILE_PATH to fileInfo.file.absolutePath,
FILE_LENGTH to fileInfo.length,
FILE_RANDOM to fileInfo.random
)
var updated = false
if (sticker.isCover) {
// Archive restore inserts cover rows without a sticker id, try to update first on a reduced uniqueness constraint
updated = writableDatabase
.update(TABLE_NAME)
.values(values)
.where("$PACK_ID = ? AND $COVER = 1", sticker.packId)
.run() > 0
}
if (!updated) {
writableDatabase
.insertInto(TABLE_NAME)
.values(values)
.run(SQLiteDatabase.CONFLICT_REPLACE)
}
notifyStickerListeners()
@@ -454,7 +469,7 @@ class StickerTable(
}
}
class StickerPackRecordReader(private val cursor: Cursor) : Closeable {
class StickerPackRecordReader(private val cursor: Cursor) : Closeable, Iterable<StickerPackRecord> {
fun getNext(): StickerPackRecord? {
if (!cursor.moveToNext()) {
@@ -486,5 +501,19 @@ class StickerTable(
override fun close() {
cursor.close()
}
override fun iterator(): Iterator<StickerPackRecord> {
return ReaderIterator()
}
private inner class ReaderIterator : Iterator<StickerPackRecord> {
override fun hasNext(): Boolean {
return cursor.count != 0 && !cursor.isLast
}
override fun next(): StickerPackRecord {
return getNext() ?: throw NoSuchElementException()
}
}
}
}