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.
This commit is contained in:
jeffrey-signal
2025-08-08 10:28:08 -04:00
committed by Greyson Parrelli
parent a549fff6fa
commit 784a64c353
10 changed files with 378 additions and 98 deletions

View File

@@ -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<Integer, Integer> size = BitmapUtil.getDimensions(stream);
return size.first < DIMENSION_LIMIT && size.second < DIMENSION_LIMIT;
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;
}

View File

@@ -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<DecryptableUri, InputStream> {
private final Context context;
private DecryptableStreamUriLoader(Context context) {
this.context = context;
}
@Nullable
@Override
public LoadData<InputStream> 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<DecryptableUri, InputStream> {
private final Context context;
Factory(Context context) {
this.context = context.getApplicationContext();
}
@Override
public @NonNull ModelLoader<DecryptableUri, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) {
return new DecryptableStreamUriLoader(context);
}
@Override
public void teardown() {
// Do nothing.
}
}
}

View File

@@ -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<InputStreamFactory> {
override fun getDataClass(): Class<InputStreamFactory> = InputStreamFactory::class.java
override fun getDataSource(): DataSource = DataSource.LOCAL
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStreamFactory>) {
try {
callback.onDataReady(InputStreamFactory.build(context, decryptableUri.uri))
} catch (e: Exception) {
callback.onLoadFailed(e)
}
}
override fun cancel() = Unit
override fun cleanup() = Unit
}

View File

@@ -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<DecryptableUri, InputStreamFactory> {
override fun handles(model: DecryptableUri): Boolean = true
override fun buildLoadData(
model: DecryptableUri,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStreamFactory> {
val sourceKey = ObjectKey(model)
val dataFetcher = DecryptableUriStreamFetcher(context, model)
return ModelLoader.LoadData(sourceKey, dataFetcher)
}
class Factory(
private val context: Context
) : ModelLoaderFactory<DecryptableUri, InputStreamFactory> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<DecryptableUri, InputStreamFactory> {
return DecryptableUriStreamLoader(context)
}
override fun teardown() = Unit
}
}

View File

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

View File

@@ -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<InputStreamFactory, Bitmap> {
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<Bitmap?>? {
return downsampler.decode(source, width, height, options)
}
}

View File

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