From 784a64c3532e395ac8c240d433554ab3c9105f8b Mon Sep 17 00:00:00 2001 From: jeffrey-signal Date: Fri, 8 Aug 2025 10:28:08 -0400 Subject: [PATCH] Fix large image file loading failures. Replaces `DecryptableStreamUriLoader` with `DecryptableUriStreamLoader`, which addresses `InvalidMarkException` errors that were occurring when loading large image files with Glide. This new model loader provides a more robust approach via multiple fallback mechanisms to try to recover gracefully from errors related to displaying large images. --- .../securesms/glide/Downsampler.java | 25 ++- .../securesms/glide/ImageHeaderParserUtils.kt | 150 ++++++++++++++++++ .../securesms/glide/ImageReader.java | 52 +++--- .../mms/DecryptableStreamLocalUriFetcher.java | 9 +- .../mms/DecryptableStreamUriLoader.java | 53 ------- .../mms/DecryptableUriStreamFetcher.kt | 34 ++++ .../mms/DecryptableUriStreamLoader.kt | 44 +++++ .../securesms/mms/InputStreamFactory.kt | 69 ++++++++ .../mms/InputStreamFactoryBitmapDecoder.kt | 37 +++++ .../securesms/mms/SignalGlideComponents.java | 3 +- 10 files changed, 378 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/glide/ImageHeaderParserUtils.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableUriStreamFetcher.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableUriStreamLoader.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mms/InputStreamFactory.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mms/InputStreamFactoryBitmapDecoder.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/Downsampler.java b/app/src/main/java/org/thoughtcrime/securesms/glide/Downsampler.java index 873de754f7..7cb17e004f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/Downsampler.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/Downsampler.java @@ -14,6 +14,7 @@ import android.os.Build; import android.os.ParcelFileDescriptor; import android.util.DisplayMetrics; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; @@ -39,6 +40,7 @@ import com.bumptech.glide.util.Preconditions; import com.bumptech.glide.util.Util; import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.mms.InputStreamFactory; import java.io.File; import java.io.IOException; @@ -184,22 +186,17 @@ public final class Downsampler { * data present in the stream and that is downsampled according to the given dimensions and any * provided {@link com.bumptech.glide.load.resource.bitmap.DownsampleStrategy} option. * - * @see #decode(InputStream, int, int, Options, DecodeCallbacks) + * @see #decode(InputStreamFactory, int, int, Options, DecodeCallbacks) */ - public Resource decode(InputStream is, int outWidth, int outHeight, Options options) - throws IOException - { - return decode(is, outWidth, outHeight, options, EMPTY_CALLBACKS); + public Resource decode(@NonNull InputStreamFactory inputStreamFactory, int outWidth, int outHeight, Options options) throws IOException { + return decode(inputStreamFactory, outWidth, outHeight, options, EMPTY_CALLBACKS); } /** - * Identical to {@link #decode(InputStream, int, int, Options)}, except that it accepts a {@link - * ByteBuffer} in place of an {@link InputStream}. + * Identical to {@link #decode(InputStreamFactory, int, int, Options)}, except that it accepts a {@link + * ByteBuffer} in place of an {@link InputStreamFactory}. */ - public Resource decode( - ByteBuffer buffer, int requestedWidth, int requestedHeight, Options options) - throws IOException - { + public Resource decode(ByteBuffer buffer, int requestedWidth, int requestedHeight, Options options) throws IOException { return decode( new ImageReader.ByteBufferReader(buffer, parsers, byteArrayPool), requestedWidth, @@ -218,7 +215,7 @@ public final class Downsampler { * of the image for the given InputStream is available, the operation is much less expensive in * terms of memory. * - * @param is An {@link InputStream} to the data for the image. + * @param inputStreamFactory An {@link InputStreamFactory} to the data for the image. * @param requestedWidth The width the final image should be close to. * @param requestedHeight The height the final image should be close to. * @param options A set of options that may contain one or more supported options that influence @@ -229,7 +226,7 @@ public final class Downsampler { * not null. */ public Resource decode( - InputStream is, + @NonNull InputStreamFactory inputStreamFactory, int requestedWidth, int requestedHeight, Options options, @@ -237,7 +234,7 @@ public final class Downsampler { throws IOException { return decode( - new ImageReader.InputStreamImageReader(is, parsers, byteArrayPool), + new ImageReader.InputStreamImageReader(inputStreamFactory, parsers, byteArrayPool), requestedWidth, requestedHeight, options, diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/ImageHeaderParserUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/glide/ImageHeaderParserUtils.kt new file mode 100644 index 0000000000..a710ae48ae --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/ImageHeaderParserUtils.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.glide + +import androidx.exifinterface.media.ExifInterface +import com.bumptech.glide.load.ImageHeaderParser +import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder +import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool +import org.thoughtcrime.securesms.mms.InputStreamFactory +import org.thoughtcrime.securesms.util.BitmapUtil +import java.io.IOException +import java.io.InputStream +import java.nio.ByteBuffer + +typealias GlideImageHeaderParserUtils = com.bumptech.glide.load.ImageHeaderParserUtils + +object ImageHeaderParserUtils { + /** + * @see com.bumptech.glide.load.ImageHeaderParserUtils.getType + */ + @JvmStatic + @Throws(IOException::class) + fun getType( + parsers: List, + inputStream: InputStream, + byteArrayPool: ArrayPool + ): ImageHeaderParser.ImageType { + return GlideImageHeaderParserUtils.getType(parsers, inputStream, byteArrayPool) + } + + /** + * @see com.bumptech.glide.load.ImageHeaderParserUtils.getType + */ + @JvmStatic + @Throws(IOException::class) + fun getType( + parsers: List, + buffer: ByteBuffer + ): ImageHeaderParser.ImageType { + return GlideImageHeaderParserUtils.getType(parsers, buffer) + } + + /** + * @see com.bumptech.glide.load.ImageHeaderParserUtils.getType + */ + @JvmStatic + @Throws(IOException::class) + fun getType( + parsers: List, + parcelFileDescriptorRewinder: ParcelFileDescriptorRewinder, + byteArrayPool: ArrayPool + ): ImageHeaderParser.ImageType { + return GlideImageHeaderParserUtils.getType(parsers, parcelFileDescriptorRewinder, byteArrayPool) + } + + /** + * @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation + */ + @JvmStatic + @Throws(IOException::class) + fun getOrientationWithFallbacks( + parsers: List, + buffer: ByteBuffer, + arrayPool: ArrayPool + ): Int { + return GlideImageHeaderParserUtils.getOrientation(parsers, buffer, arrayPool) + } + + /** + * @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation + */ + @JvmStatic + @Throws(IOException::class) + fun getOrientation( + parsers: List, + parcelFileDescriptorRewinder: ParcelFileDescriptorRewinder, + byteArrayPool: ArrayPool + ): Int { + return GlideImageHeaderParserUtils.getOrientation(parsers, parcelFileDescriptorRewinder, byteArrayPool) + } + + @JvmStatic + @Throws(IOException::class) + fun getOrientation( + parsers: List, + inputStream: InputStream, + byteArrayPool: ArrayPool + ): Int { + return GlideImageHeaderParserUtils.getOrientation(parsers, inputStream, byteArrayPool) + } + + /** + * @see com.bumptech.glide.load.ImageHeaderParserUtils.getOrientation + */ + @JvmStatic + @Throws(IOException::class) + fun getOrientationWithFallbacks( + parsers: List, + inputStreamFactory: InputStreamFactory, + byteArrayPool: ArrayPool + ): Int { + val orientationFromParsers = getOrientationFromParsers( + parsers = parsers, + inputStream = inputStreamFactory.createRecyclable(byteArrayPool), + byteArrayPool = byteArrayPool + ) + if (orientationFromParsers != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromParsers + + val orientationFromExif = getOrientationFromExif(inputStream = inputStreamFactory.createRecyclable(byteArrayPool)) + if (orientationFromExif != ImageHeaderParser.UNKNOWN_ORIENTATION) return orientationFromExif + + return ImageHeaderParser.UNKNOWN_ORIENTATION + } + + private fun getOrientationFromParsers( + parsers: List, + inputStream: InputStream?, + byteArrayPool: ArrayPool + ): Int { + if (inputStream == null) { + return ImageHeaderParser.UNKNOWN_ORIENTATION + } + + return getOrientation( + parsers = parsers, + readOrientation = { parser -> parser.getOrientation(inputStream, byteArrayPool) } + ) + } + + private fun getOrientationFromExif(inputStream: InputStream): Int { + return BitmapUtil.getExifOrientation(ExifInterface(inputStream)) + } + + private fun getOrientation( + parsers: List, + readOrientation: (ImageHeaderParser) -> Int + ): Int { + parsers.forEach { parser -> + val orientation = readOrientation(parser) + if (orientation != ImageHeaderParser.UNKNOWN_ORIENTATION) { + return orientation + } + } + + return ImageHeaderParser.UNKNOWN_ORIENTATION + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/ImageReader.java b/app/src/main/java/org/thoughtcrime/securesms/glide/ImageReader.java index 30cbfa2724..d9aea0fdba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/ImageReader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/ImageReader.java @@ -11,12 +11,12 @@ import android.graphics.BitmapFactory.Options; import android.os.Build; import android.os.ParcelFileDescriptor; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.bumptech.glide.load.ImageHeaderParser; import com.bumptech.glide.load.ImageHeaderParser.ImageType; -import com.bumptech.glide.load.ImageHeaderParserUtils; import com.bumptech.glide.load.data.DataRewinder; import com.bumptech.glide.load.data.InputStreamRewinder; import com.bumptech.glide.load.data.ParcelFileDescriptorRewinder; @@ -25,6 +25,8 @@ import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream; import com.bumptech.glide.util.ByteBufferUtil; import com.bumptech.glide.util.Preconditions; +import org.thoughtcrime.securesms.mms.InputStreamFactory; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -72,7 +74,7 @@ interface ImageReader { @Override public int getImageOrientation() throws IOException { - return ImageHeaderParserUtils.getOrientation(parsers, ByteBuffer.wrap(bytes), byteArrayPool); + return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, ByteBuffer.wrap(bytes), byteArrayPool); } @Override @@ -128,19 +130,7 @@ interface ImageReader { @Override public int getImageOrientation() throws IOException { - InputStream is = null; - try { - is = new RecyclableBufferedInputStream(new FileInputStream(file), byteArrayPool); - return ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // Ignored. - } - } - } + return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, InputStreamFactory.build(file), byteArrayPool); } @Override @@ -172,8 +162,7 @@ interface ImageReader { @Override public int getImageOrientation() throws IOException { - return ImageHeaderParserUtils.getOrientation( - parsers, ByteBufferUtil.rewind(buffer), byteArrayPool); + return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, ByteBufferUtil.rewind(buffer), byteArrayPool); } @Override @@ -188,31 +177,42 @@ interface ImageReader { private final InputStreamRewinder dataRewinder; private final ArrayPool byteArrayPool; private final List parsers; + private final InputStreamFactory inputStreamFactory; - InputStreamImageReader( - InputStream is, List parsers, ArrayPool byteArrayPool) - { + InputStreamImageReader(@NonNull InputStreamFactory inputStreamFactory, List parsers, ArrayPool byteArrayPool) { this.byteArrayPool = Preconditions.checkNotNull(byteArrayPool); this.parsers = Preconditions.checkNotNull(parsers); - dataRewinder = new InputStreamRewinder(is, byteArrayPool); + this.inputStreamFactory = inputStreamFactory; + this.dataRewinder = new InputStreamRewinder(inputStreamFactory.create(), byteArrayPool); } @Nullable @Override - public Bitmap decodeBitmap(BitmapFactory.Options options) throws IOException { - return BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options); + public Bitmap decodeBitmap(@NonNull BitmapFactory.Options options) { + try { + return BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options); + } catch (IOException e) { + return BitmapFactory.decodeStream(inputStreamFactory.createRecyclable(byteArrayPool), null, options); + } } @Override public ImageHeaderParser.ImageType getImageType() throws IOException { - return ImageHeaderParserUtils.getType(parsers, dataRewinder.rewindAndGet(), byteArrayPool); + try { + return ImageHeaderParserUtils.getType(parsers, dataRewinder.rewindAndGet(), byteArrayPool); + } catch (IOException e) { + return ImageHeaderParserUtils.getType(parsers, inputStreamFactory.createRecyclable(byteArrayPool), byteArrayPool); + } } @Override public int getImageOrientation() throws IOException { - return ImageHeaderParserUtils.getOrientation( - parsers, dataRewinder.rewindAndGet(), byteArrayPool); + try { + return ImageHeaderParserUtils.getOrientation(parsers, dataRewinder.rewindAndGet(), byteArrayPool); + } catch (IOException e) { + return ImageHeaderParserUtils.getOrientationWithFallbacks(parsers, inputStreamFactory, byteArrayPool); + } } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java index b7d07c3bb3..e732df91b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamLocalUriFetcher.java @@ -25,9 +25,9 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher { private static final String TAG = Log.tag(DecryptableStreamLocalUriFetcher.class); - private static final int DIMENSION_LIMIT = 12_000; + private static final long TOTAL_PIXEL_SIZE_LIMIT = 200_000_000L; // 200 megapixels - private Context context; + private final Context context; DecryptableStreamLocalUriFetcher(Context context, Uri uri) { super(context.getContentResolver(), uri); @@ -76,8 +76,9 @@ class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher { private boolean isSafeSize(InputStream stream) { try { - Pair size = BitmapUtil.getDimensions(stream); - return size.first < DIMENSION_LIMIT && size.second < DIMENSION_LIMIT; + Pair dimensions = BitmapUtil.getDimensions(stream); + long totalPixels = (long) dimensions.first * dimensions.second; + return totalPixels < TOTAL_PIXEL_SIZE_LIMIT; } catch (BitmapDecodingException e) { return false; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java deleted file mode 100644 index 93a21b6a96..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableStreamUriLoader.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.thoughtcrime.securesms.mms; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.bumptech.glide.load.Options; -import com.bumptech.glide.load.model.ModelLoader; -import com.bumptech.glide.load.model.ModelLoaderFactory; -import com.bumptech.glide.load.model.MultiModelLoaderFactory; - -import java.io.InputStream; - -public class DecryptableStreamUriLoader implements ModelLoader { - - private final Context context; - - private DecryptableStreamUriLoader(Context context) { - this.context = context; - } - - @Nullable - @Override - public LoadData buildLoadData(@NonNull DecryptableUri decryptableUri, int width, int height, @NonNull Options options) { - return new LoadData<>(decryptableUri, new DecryptableStreamLocalUriFetcher(context, decryptableUri.getUri())); - } - - @Override - public boolean handles(@NonNull DecryptableUri decryptableUri) { - return true; - } - - static class Factory implements ModelLoaderFactory { - - private final Context context; - - Factory(Context context) { - this.context = context.getApplicationContext(); - } - - @Override - public @NonNull ModelLoader build(@NonNull MultiModelLoaderFactory multiFactory) { - return new DecryptableStreamUriLoader(context); - } - - @Override - public void teardown() { - // Do nothing. - } - } -} - diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableUriStreamFetcher.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableUriStreamFetcher.kt new file mode 100644 index 0000000000..edba2813b5 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableUriStreamFetcher.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.mms + +import android.content.Context +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.data.DataFetcher + +/** + * A Glide [DataFetcher] that retrieves an [InputStreamFactory] for a [DecryptableUri]. + */ +class DecryptableUriStreamFetcher( + private val context: Context, + private val decryptableUri: DecryptableUri +) : DataFetcher { + + override fun getDataClass(): Class = InputStreamFactory::class.java + override fun getDataSource(): DataSource = DataSource.LOCAL + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + try { + callback.onDataReady(InputStreamFactory.build(context, decryptableUri.uri)) + } catch (e: Exception) { + callback.onLoadFailed(e) + } + } + + override fun cancel() = Unit + override fun cleanup() = Unit +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableUriStreamLoader.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableUriStreamLoader.kt new file mode 100644 index 0000000000..2052a33dfd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/DecryptableUriStreamLoader.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.mms + +import android.content.Context +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.signature.ObjectKey + +/** + * A Glide [ModelLoader] that handles conversion from [DecryptableUri] to [InputStreamFactory]. + */ +class DecryptableUriStreamLoader( + private val context: Context +) : ModelLoader { + + override fun handles(model: DecryptableUri): Boolean = true + + override fun buildLoadData( + model: DecryptableUri, + width: Int, + height: Int, + options: Options + ): ModelLoader.LoadData { + val sourceKey = ObjectKey(model) + val dataFetcher = DecryptableUriStreamFetcher(context, model) + return ModelLoader.LoadData(sourceKey, dataFetcher) + } + + class Factory( + private val context: Context + ) : ModelLoaderFactory { + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return DecryptableUriStreamLoader(context) + } + + override fun teardown() = Unit + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/InputStreamFactory.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/InputStreamFactory.kt new file mode 100644 index 0000000000..e29752d7da --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/InputStreamFactory.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.mms + +import android.content.Context +import android.net.Uri +import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool +import com.bumptech.glide.load.resource.bitmap.RecyclableBufferedInputStream +import org.signal.core.util.logging.Log +import java.io.File +import java.io.FileInputStream +import java.io.InputStream + +interface InputStreamFactory { + companion object { + @JvmStatic + fun build(context: Context, uri: Uri): InputStreamFactory = UriInputStreamFactory(context, uri) + + @JvmStatic + fun build(file: File): InputStreamFactory = FileInputStreamFactory(file) + } + + fun create(): InputStream + fun createRecyclable(byteArrayPool: ArrayPool): InputStream = RecyclableBufferedInputStream(create(), byteArrayPool) +} + +/** + * A factory that creates a new [InputStream] for the given [Uri] each time [create] is called. + */ +class UriInputStreamFactory( + private val context: Context, + private val uri: Uri +) : InputStreamFactory { + companion object { + private val TAG = Log.tag(UriInputStreamFactory::class) + } + + override fun create(): InputStream { + return try { + DecryptableStreamLocalUriFetcher(context, uri).loadResource(uri, context.contentResolver) + } catch (e: Exception) { + Log.w(TAG, "Error creating input stream for URI.", e) + throw e + } + } +} + +/** + * A factory that creates a new [InputStream] for the given [File] each time [create] is called. + */ +class FileInputStreamFactory( + private val file: File +) : InputStreamFactory { + companion object { + private val TAG = Log.tag(FileInputStreamFactory::class) + } + + override fun create(): InputStream { + return try { + FileInputStream(file) + } catch (e: Exception) { + Log.w(TAG, "Error creating input stream for File.", e) + throw e + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/InputStreamFactoryBitmapDecoder.kt b/app/src/main/java/org/thoughtcrime/securesms/mms/InputStreamFactoryBitmapDecoder.kt new file mode 100644 index 0000000000..e16a907a3b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/InputStreamFactoryBitmapDecoder.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.mms + +import android.content.Context +import android.graphics.Bitmap +import com.bumptech.glide.Glide +import com.bumptech.glide.Registry +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.ResourceDecoder +import com.bumptech.glide.load.engine.Resource +import org.thoughtcrime.securesms.glide.Downsampler + +/** + * A Glide [ResourceDecoder] that decodes [Bitmap]s from a [InputStreamFactory] instances. + */ +class InputStreamFactoryBitmapDecoder( + private val downsampler: Downsampler +) : ResourceDecoder { + + constructor( + context: Context, + glide: Glide, + registry: Registry + ) : this( + downsampler = Downsampler(registry.imageHeaderParsers, context.resources.displayMetrics, glide.bitmapPool, glide.arrayPool) + ) + + override fun handles(source: InputStreamFactory, options: Options): Boolean = true + + override fun decode(source: InputStreamFactory, width: Int, height: Int, options: Options): Resource? { + return downsampler.decode(source, width, height, options) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideComponents.java b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideComponents.java index 65fb31f05f..eef7d5f9c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideComponents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/SignalGlideComponents.java @@ -93,7 +93,8 @@ public class SignalGlideComponents implements RegisterGlideComponents { registry.append(StoryTextPostModel.class, StoryTextPostModel.class, UnitModelLoader.Factory.getInstance()); registry.append(ConversationShortcutPhoto.class, Bitmap.class, new ConversationShortcutPhoto.Loader.Factory(context)); registry.append(ContactPhoto.class, InputStream.class, new ContactPhotoLoader.Factory(context)); - registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); + registry.append(DecryptableUri.class, InputStreamFactory.class, new DecryptableUriStreamLoader.Factory(context)); + registry.append(InputStreamFactory.class, Bitmap.class, new InputStreamFactoryBitmapDecoder(context, glide, registry)); registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory()); registry.append(StickerRemoteUri.class, InputStream.class, new StickerRemoteUriLoader.Factory()); registry.append(BlurHash.class, BlurHash.class, new BlurHashModelLoader.Factory());