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 {
|
||||
if (PartAuthority.isBlobUri(uri) && BlobProvider.isSingleUseMemoryBlob(uri)) {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} else if (isSafeSize(PartAuthority.getAttachmentThumbnailStream(context, uri))) {
|
||||
} else if (isSafeSize(context, uri)) {
|
||||
return PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
} else {
|
||||
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 {
|
||||
InputStream stream = PartAuthority.getAttachmentThumbnailStream(context, uri);
|
||||
Pair<Integer, Integer> dimensions = BitmapUtil.getDimensions(stream);
|
||||
long totalPixels = (long) dimensions.first * dimensions.second;
|
||||
return totalPixels < TOTAL_PIXEL_SIZE_LIMIT;
|
||||
} 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.jobs.DeleteAbandonedAttachmentsJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.QuoteThumbnailBackfillJob;
|
||||
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
|
||||
import org.thoughtcrime.securesms.migrations.QuoteThumbnailBackfillMigrationJob;
|
||||
import org.thoughtcrime.securesms.stickers.BlessedPacks;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
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()));
|
||||
EmojiSearchIndexDownloadJob.scheduleImmediately();
|
||||
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 {
|
||||
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
|
||||
)
|
||||
|
||||
private const val QUOTE_THUMBNAIL_DIMEN = 150
|
||||
private const val QUOTE_IMAGE_QUALITY = 80
|
||||
private const val QUOTE_THUMBNAIL_DIMEN = 200
|
||||
private const val QUOTE_THUMBAIL_QUALITY = 50
|
||||
const val QUOTE_PENDING_TRANSCODE = 2
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
@@ -453,17 +454,23 @@ class AttachmentTable(
|
||||
.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()) {
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val query = SqlUtil.buildFastCollectionQuery(MESSAGE_ID, mmsIds)
|
||||
val where = if (excludeTranscodingQuotes) {
|
||||
"(${query.where}) AND $QUOTE != $QUOTE_PENDING_TRANSCODE"
|
||||
} else {
|
||||
query.where
|
||||
}
|
||||
|
||||
return readableDatabase
|
||||
.select(*PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.where(query.where, query.whereArgs)
|
||||
.where(where, query.whereArgs)
|
||||
.orderBy("$ID ASC")
|
||||
.run()
|
||||
.groupBy { cursor ->
|
||||
@@ -2027,12 +2034,12 @@ class AttachmentTable(
|
||||
incrementalDigest = null,
|
||||
incrementalMacChunkSize = 0,
|
||||
fastPreflightId = jsonObject.getString(FAST_PREFLIGHT_ID),
|
||||
voiceNote = jsonObject.getInt(VOICE_NOTE) == 1,
|
||||
borderless = jsonObject.getInt(BORDERLESS) == 1,
|
||||
videoGif = jsonObject.getInt(VIDEO_GIF) == 1,
|
||||
voiceNote = jsonObject.getInt(VOICE_NOTE) != 0,
|
||||
borderless = jsonObject.getInt(BORDERLESS) != 0,
|
||||
videoGif = jsonObject.getInt(VIDEO_GIF) != 0,
|
||||
width = jsonObject.getInt(WIDTH),
|
||||
height = jsonObject.getInt(HEIGHT),
|
||||
quote = jsonObject.getInt(QUOTE) == 1,
|
||||
quote = jsonObject.getInt(QUOTE) != 0,
|
||||
caption = jsonObject.getString(CAPTION),
|
||||
stickerLocator = if (jsonObject.getInt(STICKER_ID) >= 0) {
|
||||
StickerLocator(
|
||||
@@ -2386,7 +2393,7 @@ class AttachmentTable(
|
||||
put(VIDEO_GIF, attachment.videoGif.toInt())
|
||||
put(WIDTH, attachment.width)
|
||||
put(HEIGHT, attachment.height)
|
||||
put(QUOTE, quote)
|
||||
put(QUOTE, quote.toInt())
|
||||
put(CAPTION, attachment.caption)
|
||||
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
|
||||
put(BLUR_HASH, attachment.blurHash?.hash)
|
||||
@@ -2467,7 +2474,7 @@ class AttachmentTable(
|
||||
return attachmentId
|
||||
}
|
||||
|
||||
private fun generateQuoteThumbnail(uri: DecryptableUri, contentType: String?): ImageCompressionUtil.Result? {
|
||||
fun generateQuoteThumbnail(uri: DecryptableUri, contentType: String?, quiet: Boolean = false): ImageCompressionUtil.Result? {
|
||||
return try {
|
||||
when {
|
||||
MediaUtil.isImageType(contentType) -> {
|
||||
@@ -2480,7 +2487,8 @@ class AttachmentTable(
|
||||
outputFormat,
|
||||
uri,
|
||||
QUOTE_THUMBNAIL_DIMEN,
|
||||
QUOTE_IMAGE_QUALITY
|
||||
QUOTE_THUMBAIL_QUALITY,
|
||||
true
|
||||
)
|
||||
}
|
||||
MediaUtil.isVideoType(contentType) -> {
|
||||
@@ -2492,7 +2500,7 @@ class AttachmentTable(
|
||||
MediaUtil.IMAGE_JPEG,
|
||||
uri,
|
||||
QUOTE_THUMBNAIL_DIMEN,
|
||||
QUOTE_IMAGE_QUALITY
|
||||
QUOTE_THUMBAIL_QUALITY
|
||||
)
|
||||
} else {
|
||||
Log.w(TAG, "[generateQuoteThumbnail] Failed to extract video thumbnail")
|
||||
@@ -2505,10 +2513,10 @@ class AttachmentTable(
|
||||
}
|
||||
}
|
||||
} 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
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "[generateQuoteThumbnail] Failed to generate thumbnail", e)
|
||||
Log.w(TAG, "[generateQuoteThumbnail] Failed to generate thumbnail", e.takeUnless { quiet })
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -2543,7 +2551,7 @@ class AttachmentTable(
|
||||
put(VIDEO_GIF, attachment.videoGif.toInt())
|
||||
put(WIDTH, attachment.width)
|
||||
put(HEIGHT, attachment.height)
|
||||
put(QUOTE, quote)
|
||||
put(QUOTE, quote.toInt())
|
||||
put(CAPTION, attachment.caption)
|
||||
put(UPLOAD_TIMESTAMP, attachment.uploadTimestamp)
|
||||
put(ARCHIVE_CDN, attachment.archiveCdn)
|
||||
@@ -2799,7 +2807,7 @@ class AttachmentTable(
|
||||
contentValues.put(VIDEO_GIF, if (attachment.videoGif) 1 else 0)
|
||||
contentValues.put(WIDTH, uploadTemplate?.width ?: attachment.width)
|
||||
contentValues.put(HEIGHT, uploadTemplate?.height ?: attachment.height)
|
||||
contentValues.put(QUOTE, quote)
|
||||
contentValues.put(QUOTE, quote.toInt())
|
||||
contentValues.put(CAPTION, attachment.caption)
|
||||
contentValues.put(UPLOAD_TIMESTAMP, uploadTemplate?.uploadTimestamp ?: 0)
|
||||
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(
|
||||
val file: File,
|
||||
val length: Long,
|
||||
|
||||
@@ -86,6 +86,7 @@ import org.thoughtcrime.securesms.migrations.PnpLaunchMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.PreKeysSyncMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.ProfileMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.ProfileSharingUpdateMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.QuoteThumbnailBackfillMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.RebuildMessageSearchIndexMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.RecheckPaymentsMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.RecipientSearchMigrationJob;
|
||||
@@ -233,6 +234,7 @@ public final class JobManagerFactories {
|
||||
put(PushProcessEarlyMessagesJob.KEY, new PushProcessEarlyMessagesJob.Factory());
|
||||
put(PushProcessMessageErrorJob.KEY, new PushProcessMessageErrorJob.Factory());
|
||||
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());
|
||||
put(QuoteThumbnailBackfillJob.KEY, new QuoteThumbnailBackfillJob.Factory());
|
||||
put(ReactionSendJob.KEY, new ReactionSendJob.Factory());
|
||||
put(RebuildMessageSearchIndexJob.KEY, new RebuildMessageSearchIndexJob.Factory());
|
||||
put(ReclaimUsernameAndLinkJob.KEY, new ReclaimUsernameAndLinkJob.Factory());
|
||||
@@ -326,6 +328,7 @@ public final class JobManagerFactories {
|
||||
put(PreKeysSyncMigrationJob.KEY, new PreKeysSyncMigrationJob.Factory());
|
||||
put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory());
|
||||
put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory());
|
||||
put(QuoteThumbnailBackfillMigrationJob.KEY, new QuoteThumbnailBackfillMigrationJob.Factory());
|
||||
put(RebuildMessageSearchIndexMigrationJob.KEY, new RebuildMessageSearchIndexMigrationJob.Factory());
|
||||
put(RecheckPaymentsMigrationJob.KEY, new RecheckPaymentsMigrationJob.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 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 STARTED_QUOTE_THUMBNAIL_MIGRATION = "misc.started_quote_thumbnail_migration"
|
||||
}
|
||||
|
||||
public override fun onFirstEverAppLaunch() {
|
||||
putLong(MESSAGE_REQUEST_ENABLE_TIME, 0)
|
||||
putBoolean(NEEDS_USERNAME_RESTORE, true)
|
||||
putBoolean(STARTED_QUOTE_THUMBNAIL_MIGRATION, true)
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
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 RESET_ARCHIVE_TIER = 144;
|
||||
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
|
||||
@@ -864,6 +865,10 @@ public class ApplicationMigrations {
|
||||
jobs.put(Version.ARCHIVE_BACKUP_ID, new ArchiveBackupIdReservationMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.QUOTE_THUMBNAIL_BACKFILL) {
|
||||
jobs.put(Version.QUOTE_THUMBNAIL_BACKFILL, new QuoteThumbnailBackfillMigrationJob());
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ public final class ImageCompressionUtil {
|
||||
}
|
||||
};
|
||||
|
||||
private ImageCompressionUtil () {}
|
||||
private ImageCompressionUtil() {}
|
||||
|
||||
/**
|
||||
* A result satisfying the provided constraints, or null if they could not be met.
|
||||
@@ -79,6 +79,22 @@ public final class ImageCompressionUtil {
|
||||
int maxDimension,
|
||||
@IntRange(from = 0, to = 100) int quality)
|
||||
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;
|
||||
|
||||
@@ -93,6 +109,10 @@ public final class ImageCompressionUtil {
|
||||
.submit(maxDimension, maxDimension)
|
||||
.get();
|
||||
} 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);
|
||||
if (e.getCause() instanceof GlideException) {
|
||||
List<Throwable> rootCauses = ((GlideException) e.getCause()).getRootCauses();
|
||||
|
||||
Reference in New Issue
Block a user