mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 12:08:34 +00:00
Add a migration to generate thumbnails for existing quotes.
This commit is contained in:
@@ -69,7 +69,7 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
|||||||
try {
|
try {
|
||||||
if (PartAuthority.isBlobUri(uri) && BlobProvider.isSingleUseMemoryBlob(uri)) {
|
if (PartAuthority.isBlobUri(uri) && BlobProvider.isSingleUseMemoryBlob(uri)) {
|
||||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||||
} else if (isSafeSize(PartAuthority.getAttachmentThumbnailStream(context, uri))) {
|
} else if (isSafeSize(context, uri)) {
|
||||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("File dimensions are too large!");
|
throw new IOException("File dimensions are too large!");
|
||||||
@@ -80,13 +80,15 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isSafeSize(InputStream stream) {
|
private boolean isSafeSize(Context context, Uri uri) throws IOException {
|
||||||
try {
|
try {
|
||||||
|
InputStream stream = PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||||
Pair<Integer, Integer> dimensions = BitmapUtil.getDimensions(stream);
|
Pair<Integer, Integer> dimensions = BitmapUtil.getDimensions(stream);
|
||||||
long totalPixels = (long) dimensions.first * dimensions.second;
|
long totalPixels = (long) dimensions.first * dimensions.second;
|
||||||
return totalPixels < TOTAL_PIXEL_SIZE_LIMIT;
|
return totalPixels < TOTAL_PIXEL_SIZE_LIMIT;
|
||||||
} catch (BitmapDecodingException e) {
|
} catch (BitmapDecodingException e) {
|
||||||
return false;
|
Long size = PartAuthority.getAttachmentSize(context, uri);
|
||||||
|
return size != null && size < GlideStreamConfig.getMarkReadLimitBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
|||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobs.DeleteAbandonedAttachmentsJob;
|
import org.thoughtcrime.securesms.jobs.DeleteAbandonedAttachmentsJob;
|
||||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||||
|
import org.thoughtcrime.securesms.jobs.QuoteThumbnailBackfillJob;
|
||||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||||
|
import org.thoughtcrime.securesms.migrations.QuoteThumbnailBackfillMigrationJob;
|
||||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
@@ -62,6 +64,12 @@ public final class AppInitialization {
|
|||||||
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
|
||||||
EmojiSearchIndexDownloadJob.scheduleImmediately();
|
EmojiSearchIndexDownloadJob.scheduleImmediately();
|
||||||
DeleteAbandonedAttachmentsJob.enqueue();
|
DeleteAbandonedAttachmentsJob.enqueue();
|
||||||
|
|
||||||
|
if (SignalStore.misc().startedQuoteThumbnailMigration()) {
|
||||||
|
AppDependencies.getJobManager().add(new QuoteThumbnailBackfillJob());
|
||||||
|
} else {
|
||||||
|
AppDependencies.getJobManager().add(new QuoteThumbnailBackfillMigrationJob());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ class ChatItemArchiveExporter(
|
|||||||
|
|
||||||
val attachmentsFuture = executor.submitTyped {
|
val attachmentsFuture = executor.submitTyped {
|
||||||
extraDataTimer.timeEvent("attachments") {
|
extraDataTimer.timeEvent("attachments") {
|
||||||
db.attachmentTable.getAttachmentsForMessages(messageIds)
|
db.attachmentTable.getAttachmentsForMessages(messageIds, excludeTranscodingQuotes = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,8 +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
|
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_THUMBNAIL_DIMEN = 200
|
||||||
private const val QUOTE_IMAGE_QUALITY = 80
|
private const val QUOTE_THUMBAIL_QUALITY = 50
|
||||||
|
const val QUOTE_PENDING_TRANSCODE = 2
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
@@ -453,17 +454,23 @@ class AttachmentTable(
|
|||||||
.flatten()
|
.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAttachmentsForMessages(mmsIds: Collection<Long?>): Map<Long, List<DatabaseAttachment>> {
|
@JvmOverloads
|
||||||
|
fun getAttachmentsForMessages(mmsIds: Collection<Long?>, excludeTranscodingQuotes: Boolean = false): Map<Long, List<DatabaseAttachment>> {
|
||||||
if (mmsIds.isEmpty()) {
|
if (mmsIds.isEmpty()) {
|
||||||
return emptyMap()
|
return emptyMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
val query = SqlUtil.buildFastCollectionQuery(MESSAGE_ID, mmsIds)
|
val query = SqlUtil.buildFastCollectionQuery(MESSAGE_ID, mmsIds)
|
||||||
|
val where = if (excludeTranscodingQuotes) {
|
||||||
|
"(${query.where}) AND $QUOTE != $QUOTE_PENDING_TRANSCODE"
|
||||||
|
} else {
|
||||||
|
query.where
|
||||||
|
}
|
||||||
|
|
||||||
return readableDatabase
|
return readableDatabase
|
||||||
.select(*PROJECTION)
|
.select(*PROJECTION)
|
||||||
.from(TABLE_NAME)
|
.from(TABLE_NAME)
|
||||||
.where(query.where, query.whereArgs)
|
.where(where, query.whereArgs)
|
||||||
.orderBy("$ID ASC")
|
.orderBy("$ID ASC")
|
||||||
.run()
|
.run()
|
||||||
.groupBy { cursor ->
|
.groupBy { cursor ->
|
||||||
@@ -2027,12 +2034,12 @@ class AttachmentTable(
|
|||||||
incrementalDigest = null,
|
incrementalDigest = null,
|
||||||
incrementalMacChunkSize = 0,
|
incrementalMacChunkSize = 0,
|
||||||
fastPreflightId = jsonObject.getString(FAST_PREFLIGHT_ID),
|
fastPreflightId = jsonObject.getString(FAST_PREFLIGHT_ID),
|
||||||
voiceNote = jsonObject.getInt(VOICE_NOTE) == 1,
|
voiceNote = jsonObject.getInt(VOICE_NOTE) != 0,
|
||||||
borderless = jsonObject.getInt(BORDERLESS) == 1,
|
borderless = jsonObject.getInt(BORDERLESS) != 0,
|
||||||
videoGif = jsonObject.getInt(VIDEO_GIF) == 1,
|
videoGif = jsonObject.getInt(VIDEO_GIF) != 0,
|
||||||
width = jsonObject.getInt(WIDTH),
|
width = jsonObject.getInt(WIDTH),
|
||||||
height = jsonObject.getInt(HEIGHT),
|
height = jsonObject.getInt(HEIGHT),
|
||||||
quote = jsonObject.getInt(QUOTE) == 1,
|
quote = jsonObject.getInt(QUOTE) != 0,
|
||||||
caption = jsonObject.getString(CAPTION),
|
caption = jsonObject.getString(CAPTION),
|
||||||
stickerLocator = if (jsonObject.getInt(STICKER_ID) >= 0) {
|
stickerLocator = if (jsonObject.getInt(STICKER_ID) >= 0) {
|
||||||
StickerLocator(
|
StickerLocator(
|
||||||
@@ -2386,7 +2393,7 @@ class AttachmentTable(
|
|||||||
put(VIDEO_GIF, attachment.videoGif.toInt())
|
put(VIDEO_GIF, attachment.videoGif.toInt())
|
||||||
put(WIDTH, attachment.width)
|
put(WIDTH, attachment.width)
|
||||||
put(HEIGHT, attachment.height)
|
put(HEIGHT, attachment.height)
|
||||||
put(QUOTE, quote)
|
put(QUOTE, quote.toInt())
|
||||||
put(CAPTION, attachment.caption)
|
put(CAPTION, attachment.caption)
|
||||||
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
|
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
|
||||||
put(BLUR_HASH, attachment.blurHash?.hash)
|
put(BLUR_HASH, attachment.blurHash?.hash)
|
||||||
@@ -2467,7 +2474,7 @@ class AttachmentTable(
|
|||||||
return attachmentId
|
return attachmentId
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateQuoteThumbnail(uri: DecryptableUri, contentType: String?): ImageCompressionUtil.Result? {
|
fun generateQuoteThumbnail(uri: DecryptableUri, contentType: String?, quiet: Boolean = false): ImageCompressionUtil.Result? {
|
||||||
return try {
|
return try {
|
||||||
when {
|
when {
|
||||||
MediaUtil.isImageType(contentType) -> {
|
MediaUtil.isImageType(contentType) -> {
|
||||||
@@ -2480,7 +2487,8 @@ class AttachmentTable(
|
|||||||
outputFormat,
|
outputFormat,
|
||||||
uri,
|
uri,
|
||||||
QUOTE_THUMBNAIL_DIMEN,
|
QUOTE_THUMBNAIL_DIMEN,
|
||||||
QUOTE_IMAGE_QUALITY
|
QUOTE_THUMBAIL_QUALITY,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MediaUtil.isVideoType(contentType) -> {
|
MediaUtil.isVideoType(contentType) -> {
|
||||||
@@ -2492,7 +2500,7 @@ class AttachmentTable(
|
|||||||
MediaUtil.IMAGE_JPEG,
|
MediaUtil.IMAGE_JPEG,
|
||||||
uri,
|
uri,
|
||||||
QUOTE_THUMBNAIL_DIMEN,
|
QUOTE_THUMBNAIL_DIMEN,
|
||||||
QUOTE_IMAGE_QUALITY
|
QUOTE_THUMBAIL_QUALITY
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "[generateQuoteThumbnail] Failed to extract video thumbnail")
|
Log.w(TAG, "[generateQuoteThumbnail] Failed to extract video thumbnail")
|
||||||
@@ -2505,10 +2513,10 @@ class AttachmentTable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: BitmapDecodingException) {
|
} catch (e: BitmapDecodingException) {
|
||||||
Log.w(TAG, "[generateQuoteThumbnail] Failed to decode image for thumbnail", e)
|
Log.w(TAG, "[generateQuoteThumbnail] Failed to decode image for thumbnail", e.takeUnless { quiet })
|
||||||
null
|
null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "[generateQuoteThumbnail] Failed to generate thumbnail", e)
|
Log.w(TAG, "[generateQuoteThumbnail] Failed to generate thumbnail", e.takeUnless { quiet })
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2543,7 +2551,7 @@ class AttachmentTable(
|
|||||||
put(VIDEO_GIF, attachment.videoGif.toInt())
|
put(VIDEO_GIF, attachment.videoGif.toInt())
|
||||||
put(WIDTH, attachment.width)
|
put(WIDTH, attachment.width)
|
||||||
put(HEIGHT, attachment.height)
|
put(HEIGHT, attachment.height)
|
||||||
put(QUOTE, quote)
|
put(QUOTE, quote.toInt())
|
||||||
put(CAPTION, attachment.caption)
|
put(CAPTION, attachment.caption)
|
||||||
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
|
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
|
||||||
put(ARCHIVE_CDN, attachment.archiveCdn)
|
put(ARCHIVE_CDN, attachment.archiveCdn)
|
||||||
@@ -2799,7 +2807,7 @@ class AttachmentTable(
|
|||||||
contentValues.put(VIDEO_GIF, if (attachment.videoGif) 1 else 0)
|
contentValues.put(VIDEO_GIF, if (attachment.videoGif) 1 else 0)
|
||||||
contentValues.put(WIDTH, uploadTemplate?.width ?: attachment.width)
|
contentValues.put(WIDTH, uploadTemplate?.width ?: attachment.width)
|
||||||
contentValues.put(HEIGHT, uploadTemplate?.height ?: attachment.height)
|
contentValues.put(HEIGHT, uploadTemplate?.height ?: attachment.height)
|
||||||
contentValues.put(QUOTE, quote)
|
contentValues.put(QUOTE, quote.toInt())
|
||||||
contentValues.put(CAPTION, attachment.caption)
|
contentValues.put(CAPTION, attachment.caption)
|
||||||
contentValues.put(UPLOAD_TIMESTAMP, uploadTemplate?.uploadTimestamp ?: 0)
|
contentValues.put(UPLOAD_TIMESTAMP, uploadTemplate?.uploadTimestamp ?: 0)
|
||||||
contentValues.put(TRANSFORM_PROPERTIES, transformProperties.serialize())
|
contentValues.put(TRANSFORM_PROPERTIES, transformProperties.serialize())
|
||||||
@@ -3130,6 +3138,38 @@ class AttachmentTable(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in an app migration that creates quote thumbnails. Updates all quote attachments that share the same
|
||||||
|
* [previousDataFile] to use the new thumbnail.
|
||||||
|
*
|
||||||
|
* Handling deduping shouldn't be necessary here because we're updating by the dataFile we used to generate
|
||||||
|
* the thumbnail. It *is* theoretically possible that generating thumbnails for two different dataFiles
|
||||||
|
* could result in the same output thumbnail... but that's fine. That rare scenario will result in some missed
|
||||||
|
* disk savings.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun migrationFinalizeQuoteWithData(previousDataFile: String, thumbnail: ImageCompressionUtil.Result): String {
|
||||||
|
val newDataFileInfo = writeToDataFile(newDataFile(context), thumbnail.data.inputStream(), TransformProperties.empty())
|
||||||
|
|
||||||
|
writableDatabase
|
||||||
|
.update(TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
DATA_FILE to newDataFileInfo.file.absolutePath,
|
||||||
|
DATA_SIZE to newDataFileInfo.length,
|
||||||
|
DATA_RANDOM to newDataFileInfo.random,
|
||||||
|
DATA_HASH_START to newDataFileInfo.hash,
|
||||||
|
DATA_HASH_END to newDataFileInfo.hash,
|
||||||
|
CONTENT_TYPE to thumbnail.mimeType,
|
||||||
|
WIDTH to thumbnail.width,
|
||||||
|
HEIGHT to thumbnail.height,
|
||||||
|
QUOTE to 1
|
||||||
|
)
|
||||||
|
.where("$DATA_FILE = ? AND $QUOTE != 0", previousDataFile)
|
||||||
|
.run()
|
||||||
|
|
||||||
|
return newDataFileInfo.file.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
class DataFileWriteResult(
|
class DataFileWriteResult(
|
||||||
val file: File,
|
val file: File,
|
||||||
val length: Long,
|
val length: Long,
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.migrations.PnpLaunchMigrationJob;
|
|||||||
import org.thoughtcrime.securesms.migrations.PreKeysSyncMigrationJob;
|
import org.thoughtcrime.securesms.migrations.PreKeysSyncMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.ProfileMigrationJob;
|
import org.thoughtcrime.securesms.migrations.ProfileMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob;
|
import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob;
|
||||||
|
import org.thoughtcrime.securesms.migrations.QuoteThumbnailBackfillMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.RebuildMessageSearchIndexMigrationJob;
|
import org.thoughtcrime.securesms.migrations.RebuildMessageSearchIndexMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.RecheckPaymentsMigrationJob;
|
import org.thoughtcrime.securesms.migrations.RecheckPaymentsMigrationJob;
|
||||||
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
|
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
|
||||||
@@ -233,6 +234,7 @@ public final class JobManagerFactories {
|
|||||||
put(PushProcessEarlyMessagesJob.KEY, new PushProcessEarlyMessagesJob.Factory());
|
put(PushProcessEarlyMessagesJob.KEY, new PushProcessEarlyMessagesJob.Factory());
|
||||||
put(PushProcessMessageErrorJob.KEY, new PushProcessMessageErrorJob.Factory());
|
put(PushProcessMessageErrorJob.KEY, new PushProcessMessageErrorJob.Factory());
|
||||||
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());
|
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());
|
||||||
|
put(QuoteThumbnailBackfillJob.KEY, new QuoteThumbnailBackfillJob.Factory());
|
||||||
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
|
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
|
||||||
put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory());
|
put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory());
|
||||||
put(ReclaimUsernameAndLinkJob.KEY, new ReclaimUsernameAndLinkJob.Factory());
|
put(ReclaimUsernameAndLinkJob.KEY, new ReclaimUsernameAndLinkJob.Factory());
|
||||||
@@ -326,6 +328,7 @@ public final class JobManagerFactories {
|
|||||||
put(PreKeysSyncMigrationJob.KEY, new PreKeysSyncMigrationJob.Factory());
|
put(PreKeysSyncMigrationJob.KEY, new PreKeysSyncMigrationJob.Factory());
|
||||||
put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory());
|
put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory());
|
||||||
put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory());
|
put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory());
|
||||||
|
put(QuoteThumbnailBackfillMigrationJob.KEY, new QuoteThumbnailBackfillMigrationJob.Factory());
|
||||||
put(RebuildMessageSearchIndexMigrationJob.KEY, new RebuildMessageSearchIndexMigrationJob.Factory());
|
put(RebuildMessageSearchIndexMigrationJob.KEY, new RebuildMessageSearchIndexMigrationJob.Factory());
|
||||||
put(RecheckPaymentsMigrationJob.KEY, new RecheckPaymentsMigrationJob.Factory());
|
put(RecheckPaymentsMigrationJob.KEY, new RecheckPaymentsMigrationJob.Factory());
|
||||||
put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory());
|
put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory());
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.readToSingleObject
|
||||||
|
import org.signal.core.util.requireLong
|
||||||
|
import org.signal.core.util.requireNonNullString
|
||||||
|
import org.signal.core.util.requireString
|
||||||
|
import org.signal.core.util.select
|
||||||
|
import org.signal.core.util.update
|
||||||
|
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.CONTENT_TYPE
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_FILE
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_HASH_END
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_HASH_START
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_RANDOM
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_SIZE
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.ID
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.QUOTE
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.QUOTE_PENDING_TRANSCODE
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.TABLE_NAME
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
|
import org.thoughtcrime.securesms.mms.DecryptableUri
|
||||||
|
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This job processes quote attachments to generate thumbnails where possible.
|
||||||
|
* In order to avoid hammering the device, this job will process a single attachment
|
||||||
|
* and then reschedule itself to run again if necessary.
|
||||||
|
*/
|
||||||
|
class QuoteThumbnailBackfillJob private constructor(parameters: Parameters) : Job(parameters) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = Log.tag(QuoteThumbnailBackfillJob::class.java)
|
||||||
|
const val KEY = "QuoteThumbnailBackfillJob"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var activeAttachmentInfo: AttachmentInfo? = null
|
||||||
|
|
||||||
|
constructor() : this(
|
||||||
|
Parameters.Builder()
|
||||||
|
.setQueue(KEY)
|
||||||
|
.setMaxInstancesForFactory(2)
|
||||||
|
.setLifespan(Parameters.IMMORTAL)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun serialize() = null
|
||||||
|
|
||||||
|
override fun getFactoryKey() = KEY
|
||||||
|
|
||||||
|
override fun run(): Result {
|
||||||
|
for (i in 1..10) {
|
||||||
|
val complete = peformSingleBackfill()
|
||||||
|
if (complete) {
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppDependencies.jobManager.add(QuoteThumbnailBackfillJob())
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns true if the entire backfill process is complete, otherwise false */
|
||||||
|
private fun peformSingleBackfill(): Boolean {
|
||||||
|
val attachment = SignalDatabase.attachments.getNextQuoteAttachmentForThumbnailProcessing()
|
||||||
|
|
||||||
|
if (attachment == null) {
|
||||||
|
Log.i(TAG, "No more quote attachments to process! Task complete.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
activeAttachmentInfo = attachment
|
||||||
|
|
||||||
|
val thumbnail = SignalDatabase.attachments.generateQuoteThumbnail(DecryptableUri(attachment.uri), attachment.contentType, quiet = true)
|
||||||
|
if (thumbnail != null) {
|
||||||
|
SignalDatabase.attachments.migrationFinalizeQuoteWithData(attachment.dataFile, thumbnail)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to generate thumbnail for attachment: ${attachment.id}. Clearing data.")
|
||||||
|
SignalDatabase.attachments.finalizeQuoteWithNoData(attachment.dataFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure() {
|
||||||
|
activeAttachmentInfo?.let { attachment ->
|
||||||
|
Log.w(TAG, "Failed during thumbnail generation. Clearing the quote data and continuing.", true)
|
||||||
|
SignalDatabase.attachments.finalizeQuoteWithNoData(attachment.dataFile)
|
||||||
|
} ?: Log.w(TAG, "Job failed, but no active file is set!")
|
||||||
|
|
||||||
|
AppDependencies.jobManager.add(QuoteThumbnailBackfillJob())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the next quote that has a scheduled thumbnail generation, favoring newer ones. */
|
||||||
|
private fun AttachmentTable.getNextQuoteAttachmentForThumbnailProcessing(): AttachmentInfo? {
|
||||||
|
return readableDatabase
|
||||||
|
.select(ID, DATA_FILE, CONTENT_TYPE)
|
||||||
|
.from(TABLE_NAME)
|
||||||
|
.where("$QUOTE = $QUOTE_PENDING_TRANSCODE")
|
||||||
|
.orderBy("$ID DESC")
|
||||||
|
.limit(1)
|
||||||
|
.run()
|
||||||
|
.readToSingleObject {
|
||||||
|
AttachmentInfo(
|
||||||
|
id = AttachmentId(it.requireLong(ID)),
|
||||||
|
dataFile = it.requireNonNullString(DATA_FILE),
|
||||||
|
contentType = it.requireString(CONTENT_TYPE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Finalizes all quote attachments that share the given [dataFile] with empty data (because we could not generate a thumbnail). */
|
||||||
|
private fun AttachmentTable.finalizeQuoteWithNoData(dataFile: String) {
|
||||||
|
writableDatabase
|
||||||
|
.update(TABLE_NAME)
|
||||||
|
.values(
|
||||||
|
DATA_FILE to null,
|
||||||
|
DATA_RANDOM to null,
|
||||||
|
DATA_HASH_START to null,
|
||||||
|
DATA_HASH_END to null,
|
||||||
|
DATA_SIZE to 0,
|
||||||
|
QUOTE to 1
|
||||||
|
)
|
||||||
|
.where("$DATA_FILE = ? AND $QUOTE != 0 ", dataFile)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class AttachmentInfo(
|
||||||
|
val id: AttachmentId,
|
||||||
|
val dataFile: String,
|
||||||
|
val contentType: String?
|
||||||
|
) {
|
||||||
|
val uri: Uri get() = PartAuthority.getAttachmentDataUri(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory : Job.Factory<QuoteThumbnailBackfillJob> {
|
||||||
|
override fun create(parameters: Parameters, serializedData: ByteArray?): QuoteThumbnailBackfillJob {
|
||||||
|
return QuoteThumbnailBackfillJob(parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,15 +41,17 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
|
|||||||
private const val LAST_CONNECTIVITY_WARNING_TIME = "misc.last_connectivity_warning_time"
|
private const val LAST_CONNECTIVITY_WARNING_TIME = "misc.last_connectivity_warning_time"
|
||||||
private const val NEW_LINKED_DEVICE_ID = "misc.new_linked_device_id"
|
private const val NEW_LINKED_DEVICE_ID = "misc.new_linked_device_id"
|
||||||
private const val NEW_LINKED_DEVICE_CREATED_TIME = "misc.new_linked_device_created_time"
|
private const val NEW_LINKED_DEVICE_CREATED_TIME = "misc.new_linked_device_created_time"
|
||||||
|
private const val STARTED_QUOTE_THUMBNAIL_MIGRATION = "misc.started_quote_thumbnail_migration"
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun onFirstEverAppLaunch() {
|
public override fun onFirstEverAppLaunch() {
|
||||||
putLong(MESSAGE_REQUEST_ENABLE_TIME, 0)
|
putLong(MESSAGE_REQUEST_ENABLE_TIME, 0)
|
||||||
putBoolean(NEEDS_USERNAME_RESTORE, true)
|
putBoolean(NEEDS_USERNAME_RESTORE, true)
|
||||||
|
putBoolean(STARTED_QUOTE_THUMBNAIL_MIGRATION, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun getKeysToIncludeInBackup(): List<String> {
|
public override fun getKeysToIncludeInBackup(): List<String> {
|
||||||
return emptyList()
|
return listOf(STARTED_QUOTE_THUMBNAIL_MIGRATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -277,4 +279,13 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
|
|||||||
* The time, in milliseconds, that the device was created at
|
* The time, in milliseconds, that the device was created at
|
||||||
*/
|
*/
|
||||||
var newLinkedDeviceCreatedTime: Long by longValue(NEW_LINKED_DEVICE_CREATED_TIME, 0)
|
var newLinkedDeviceCreatedTime: Long by longValue(NEW_LINKED_DEVICE_CREATED_TIME, 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not we have started the quote thumbnail migration. We store this so that upon restoring from
|
||||||
|
* a local backup, we can know whether or not the user marked all of the quotes that need conversion in
|
||||||
|
* the database. If so, we can enqueue a job to continue any pending conversions, and if not we can start
|
||||||
|
* the conversion process from scratch.
|
||||||
|
*/
|
||||||
|
@get:JvmName("startedQuoteThumbnailMigration")
|
||||||
|
var startedQuoteThumbnailMigration: Boolean by booleanValue(STARTED_QUOTE_THUMBNAIL_MIGRATION, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,9 +187,10 @@ public class ApplicationMigrations {
|
|||||||
static final int SVR2_ENCLAVE_UPDATE_4 = 143;
|
static final int SVR2_ENCLAVE_UPDATE_4 = 143;
|
||||||
static final int RESET_ARCHIVE_TIER = 144;
|
static final int RESET_ARCHIVE_TIER = 144;
|
||||||
static final int ARCHIVE_BACKUP_ID = 145;
|
static final int ARCHIVE_BACKUP_ID = 145;
|
||||||
|
static final int QUOTE_THUMBNAIL_BACKFILL = 146;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final int CURRENT_VERSION = 145;
|
public static final int CURRENT_VERSION = 146;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||||
@@ -864,6 +865,10 @@ public class ApplicationMigrations {
|
|||||||
jobs.put(Version.ARCHIVE_BACKUP_ID, new ArchiveBackupIdReservationMigrationJob());
|
jobs.put(Version.ARCHIVE_BACKUP_ID, new ArchiveBackupIdReservationMigrationJob());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastSeenVersion < Version.QUOTE_THUMBNAIL_BACKFILL) {
|
||||||
|
jobs.put(Version.QUOTE_THUMBNAIL_BACKFILL, new QuoteThumbnailBackfillMigrationJob());
|
||||||
|
}
|
||||||
|
|
||||||
return jobs;
|
return jobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.migrations
|
||||||
|
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.core.util.update
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_FILE
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.QUOTE
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.QUOTE_PENDING_TRANSCODE
|
||||||
|
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.TABLE_NAME
|
||||||
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
|
import org.thoughtcrime.securesms.jobmanager.Job
|
||||||
|
import org.thoughtcrime.securesms.jobs.QuoteThumbnailBackfillJob
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import java.lang.Exception
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kicks off the quote attachment thumbnail generation process by marking quote attachments
|
||||||
|
* for processing and enqueueing a [QuoteThumbnailBackfillJob].
|
||||||
|
*/
|
||||||
|
internal class QuoteThumbnailBackfillMigrationJob(parameters: Parameters = Parameters.Builder().build()) : MigrationJob(parameters) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = Log.tag(QuoteThumbnailBackfillMigrationJob::class.java)
|
||||||
|
const val KEY = "QuoteThumbnailBackfillMigrationJob"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFactoryKey(): String = KEY
|
||||||
|
|
||||||
|
override fun isUiBlocking(): Boolean = false
|
||||||
|
|
||||||
|
override fun performMigration() {
|
||||||
|
val markedCount = SignalDatabase.attachments.migrationMarkQuoteAttachmentsForThumbnailProcessing()
|
||||||
|
SignalStore.misc.startedQuoteThumbnailMigration = true
|
||||||
|
|
||||||
|
Log.i(TAG, "Marked $markedCount quote attachments for thumbnail processing")
|
||||||
|
|
||||||
|
if (markedCount > 0) {
|
||||||
|
AppDependencies.jobManager.add(QuoteThumbnailBackfillJob())
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "No quote attachments to process.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldRetry(e: Exception): Boolean = false
|
||||||
|
|
||||||
|
private fun AttachmentTable.migrationMarkQuoteAttachmentsForThumbnailProcessing(): Int {
|
||||||
|
return writableDatabase
|
||||||
|
.update(TABLE_NAME)
|
||||||
|
.values(QUOTE to QUOTE_PENDING_TRANSCODE)
|
||||||
|
.where("$QUOTE != 0 AND $DATA_FILE NOT NULL")
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Factory : Job.Factory<QuoteThumbnailBackfillMigrationJob> {
|
||||||
|
override fun create(parameters: Parameters, serializedData: ByteArray?): QuoteThumbnailBackfillMigrationJob {
|
||||||
|
return QuoteThumbnailBackfillMigrationJob(parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,6 +79,22 @@ public final class ImageCompressionUtil {
|
|||||||
int maxDimension,
|
int maxDimension,
|
||||||
@IntRange(from = 0, to = 100) int quality)
|
@IntRange(from = 0, to = 100) int quality)
|
||||||
throws BitmapDecodingException
|
throws BitmapDecodingException
|
||||||
|
{
|
||||||
|
return compress(context, contentType, targetContentType, glideModel, maxDimension, quality, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compresses the image to match the requested parameters.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
public static @NonNull Result compress(@NonNull Context context,
|
||||||
|
@Nullable String contentType,
|
||||||
|
@Nullable String targetContentType,
|
||||||
|
@NonNull Object glideModel,
|
||||||
|
int maxDimension,
|
||||||
|
@IntRange(from = 0, to = 100) int quality,
|
||||||
|
boolean quiet)
|
||||||
|
throws BitmapDecodingException
|
||||||
{
|
{
|
||||||
Bitmap scaledBitmap;
|
Bitmap scaledBitmap;
|
||||||
|
|
||||||
@@ -93,6 +109,10 @@ public final class ImageCompressionUtil {
|
|||||||
.submit(maxDimension, maxDimension)
|
.submit(maxDimension, maxDimension)
|
||||||
.get();
|
.get();
|
||||||
} catch (ExecutionException | InterruptedException e) {
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
|
if (quiet) {
|
||||||
|
throw new BitmapDecodingException(e);
|
||||||
|
}
|
||||||
|
|
||||||
Log.w(TAG, "Verbose logging to try to give all possible debug information for Glide issues. Exceptions below may be duplicated.", e);
|
Log.w(TAG, "Verbose logging to try to give all possible debug information for Glide issues. Exceptions below may be duplicated.", e);
|
||||||
if (e.getCause() instanceof GlideException) {
|
if (e.getCause() instanceof GlideException) {
|
||||||
List<Throwable> rootCauses = ((GlideException) e.getCause()).getRootCauses();
|
List<Throwable> rootCauses = ((GlideException) e.getCause()).getRootCauses();
|
||||||
|
|||||||
Reference in New Issue
Block a user