Add a migration to generate thumbnails for existing quotes.

This commit is contained in:
Greyson Parrelli
2025-08-28 11:57:45 -04:00
parent c29d77d4a5
commit 631b51baf2
10 changed files with 328 additions and 23 deletions

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}
/**

View File

@@ -460,7 +460,7 @@ class ChatItemArchiveExporter(
val attachmentsFuture = executor.submitTyped {
extraDataTimer.timeEvent("attachments") {
db.attachmentTable.getAttachmentsForMessages(messageIds)
db.attachmentTable.getAttachmentsForMessages(messageIds, excludeTranscodingQuotes = true)
}
}

View File

@@ -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,

View File

@@ -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());

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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;
}

View File

@@ -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)
}
}
}

View File

@@ -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();