mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
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:
committed by
Greyson Parrelli
parent
a549fff6fa
commit
784a64c353
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user