Generate thumbnails for quote attachments.

This commit is contained in:
Greyson Parrelli
2025-08-26 12:54:16 -04:00
parent 71dd1d9d8b
commit d4c1c39179
22 changed files with 276 additions and 148 deletions

View File

@@ -95,12 +95,15 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.jobs.GenerateAudioWaveFormJob
import org.thoughtcrime.securesms.mms.DecryptableUri
import org.thoughtcrime.securesms.mms.MediaStream
import org.thoughtcrime.securesms.mms.MmsException
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.FileUtils
import org.thoughtcrime.securesms.util.ImageCompressionUtil
import org.thoughtcrime.securesms.util.JsonUtils.SaneJSONObject
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.RemoteConfig
@@ -299,6 +302,9 @@ class AttachmentTable(
ID, DATA_FILE, DATA_SIZE, DATA_RANDOM, DATA_HASH_START, DATA_HASH_END, TRANSFORM_PROPERTIES, UPLOAD_TIMESTAMP, ARCHIVE_CDN, ARCHIVE_TRANSFER_STATE, THUMBNAIL_FILE, THUMBNAIL_RESTORE_STATE, THUMBNAIL_RANDOM
)
private const val QUOTE_THUMBNAIL_DIMEN = 150
private const val QUOTE_IMAGE_QUALITY = 80
@JvmStatic
@Throws(IOException::class)
fun newDataFile(context: Context): File {
@@ -1789,7 +1795,7 @@ class AttachmentTable(
try {
for (attachment in quoteAttachment) {
val attachmentId = when {
attachment.uri != null -> insertAttachmentWithData(mmsId, attachment, true)
attachment.uri != null -> insertQuoteAttachment(mmsId, attachment)
attachment is ArchivedAttachment -> insertArchivedAttachment(mmsId, attachment, true)
else -> insertUndownloadedAttachment(mmsId, attachment, true)
}
@@ -2409,6 +2415,104 @@ class AttachmentTable(
return attachmentId
}
/**
* When inserting a quote attachment, it looks a lot like a normal attachment insert, but rather than insert the actual data pointed at by the attachment's
* URI, we instead want to generate a thumbnail of that attachment and use that instead.
*/
@Throws(MmsException::class)
private fun insertQuoteAttachment(messageId: Long, attachment: Attachment): AttachmentId {
Log.d(TAG, "[insertQuoteAttachment] Inserting quote attachment for messageId $messageId.")
val thumbnail = generateQuoteThumbnail(DecryptableUri(attachment.uri!!), attachment.contentType)
if (thumbnail != null) {
Log.d(TAG, "[insertQuoteAttachment] Successfully generated quote thumbnail for messageId $messageId.")
return insertAttachmentWithData(
messageId = messageId,
dataStream = thumbnail.data.inputStream(),
attachment = attachment,
quote = true
)
}
Log.d(TAG, "[insertQuoteAttachment] Unable to generate quote thumbnail for messageId $messageId. Content type: ${attachment.contentType}")
val attachmentId: AttachmentId = writableDatabase.withinTransaction { db ->
val contentValues = ContentValues().apply {
put(MESSAGE_ID, messageId)
put(CONTENT_TYPE, attachment.contentType)
put(VOICE_NOTE, attachment.voiceNote.toInt())
put(BORDERLESS, attachment.borderless.toInt())
put(VIDEO_GIF, attachment.videoGif.toInt())
put(TRANSFER_STATE, TRANSFER_PROGRESS_DONE)
put(DATA_SIZE, 0)
put(WIDTH, attachment.width)
put(HEIGHT, attachment.height)
put(QUOTE, 1)
put(BLUR_HASH, attachment.blurHash?.hash)
put(FILE_NAME, attachment.fileName)
attachment.stickerLocator?.let { sticker ->
put(STICKER_PACK_ID, sticker.packId)
put(STICKER_PACK_KEY, sticker.packKey)
put(STICKER_ID, sticker.stickerId)
put(STICKER_EMOJI, sticker.emoji)
}
}
val rowId = db.insert(TABLE_NAME, null, contentValues)
AttachmentId(rowId)
}
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
return attachmentId
}
private fun generateQuoteThumbnail(uri: DecryptableUri, contentType: String?): ImageCompressionUtil.Result? {
return try {
when {
MediaUtil.isImageType(contentType) -> {
val hasTransparency = MediaUtil.isPngType(contentType) || MediaUtil.isWebpType(contentType)
val outputFormat = if (hasTransparency) MediaUtil.IMAGE_WEBP else MediaUtil.IMAGE_JPEG
ImageCompressionUtil.compress(
context,
contentType,
outputFormat,
uri,
QUOTE_THUMBNAIL_DIMEN,
QUOTE_IMAGE_QUALITY
)
}
MediaUtil.isVideoType(contentType) -> {
val videoThumbnail = MediaUtil.getVideoThumbnail(context, uri.uri)
if (videoThumbnail != null) {
ImageCompressionUtil.compress(
context,
MediaUtil.IMAGE_JPEG,
MediaUtil.IMAGE_JPEG,
uri,
QUOTE_THUMBNAIL_DIMEN,
QUOTE_IMAGE_QUALITY
)
} else {
Log.w(TAG, "[generateQuoteThumbnail] Failed to extract video thumbnail")
null
}
}
else -> {
Log.w(TAG, "[generateQuoteThumbnail] Unsupported content type for thumbnail generation: $contentType")
null
}
}
} catch (e: BitmapDecodingException) {
Log.w(TAG, "[generateQuoteThumbnail] Failed to decode image for thumbnail", e)
null
} catch (e: Exception) {
Log.w(TAG, "[generateQuoteThumbnail] Failed to generate thumbnail", e)
null
}
}
/**
* Attachments need records in the database even if they haven't been downloaded yet. That allows us to store the info we need to download it, what message
* it's associated with, etc. We treat this case separately from attachments with data (see [insertAttachmentWithData]) because it's much simpler,

View File

@@ -2548,11 +2548,11 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val quoteText = cursor.requireString(QUOTE_BODY)
val quoteType = cursor.requireInt(QUOTE_TYPE)
val quoteMissing = cursor.requireBoolean(QUOTE_MISSING)
val quoteAttachments: List<Attachment> = associatedAttachments.filter { it.quote }.toList()
val quoteAttachment: Attachment? = associatedAttachments.filter { it.quote }.firstOrNull()
val quoteMentions: List<Mention> = parseQuoteMentions(cursor)
val quoteBodyRanges: BodyRangeList? = parseQuoteBodyRanges(cursor)
val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachments.isNotEmpty())) {
QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
val quote: QuoteModel? = if (quoteId != QUOTE_NOT_PRESENT_ID && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || quoteAttachment != null)) {
QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText ?: "", quoteMissing, quoteAttachment, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges)
} else {
null
}
@@ -2776,7 +2776,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().encode())
}
quoteAttachments += retrieved.quote.attachments
retrieved.quote.attachment?.let { quoteAttachments += it }
} else {
contentValues.put(QUOTE_ID, 0)
contentValues.put(QUOTE_AUTHOR, 0)
@@ -2869,7 +2869,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
messageId = messageId,
threadId = threadId,
threadWasNewlyCreated = threadIdResult.newlyCreated,
insertedAttachments = insertedAttachments
insertedAttachments = insertedAttachments,
quoteAttachmentId = quoteAttachments.firstOrNull()?.let { insertedAttachments?.get(it) }
)
)
}
@@ -2982,7 +2983,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
threadId: Long,
forceSms: Boolean = false,
insertListener: InsertListener? = null
): Long {
): InsertResult {
return insertMessageOutbox(
message = message,
threadId = threadId,
@@ -2999,7 +3000,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
forceSms: Boolean,
defaultReceiptStatus: Int,
insertListener: InsertListener?
): Long {
): InsertResult {
var type = MessageTypes.BASE_SENDING_TYPE
var hasSpecialType = false
@@ -3218,7 +3219,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
if (editedMessage == null) {
quoteAttachments += message.outgoingQuote.attachments
message.outgoingQuote.attachment?.let { quoteAttachments += it }
}
} else {
contentValues.put(QUOTE_ID, 0)
@@ -3320,7 +3321,13 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
TrimThreadJob.enqueueAsync(threadId)
return messageId
return InsertResult(
messageId = messageId,
threadId = threadId,
threadWasNewlyCreated = false,
insertedAttachments = insertedAttachments,
quoteAttachmentId = quoteAttachments.firstOrNull()?.let { insertedAttachments?.get(it) }
)
}
private fun hasAudioAttachment(attachments: List<Attachment>): Boolean {
@@ -5255,7 +5262,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
timetamp = this.requireLong(DATE_SENT)
),
expirationInfo = null,
storyType = StoryType.fromCode(this.requireInt(STORY_TYPE)),
storyType = fromCode(this.requireInt(STORY_TYPE)),
dateReceived = this.requireLong(DATE_RECEIVED)
)
}
@@ -5406,7 +5413,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
val messageId: Long,
val threadId: Long,
val threadWasNewlyCreated: Boolean,
val insertedAttachments: Map<Attachment, AttachmentId>? = null
val insertedAttachments: Map<Attachment, AttachmentId>? = null,
val quoteAttachmentId: AttachmentId? = null
)
data class MessageReceiptUpdate(