diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamLocalUriFetcher.java b/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamLocalUriFetcher.java new file mode 100644 index 0000000000..75094f7e1d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamLocalUriFetcher.java @@ -0,0 +1,86 @@ +package org.thoughtcrime.securesms.glide; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; +import android.util.Pair; + +import com.bumptech.glide.load.data.StreamLocalUriFetcher; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.attachments.AttachmentId; +import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.providers.BlobProvider; +import org.thoughtcrime.securesms.util.BitmapDecodingException; +import org.thoughtcrime.securesms.util.BitmapUtil; +import org.thoughtcrime.securesms.util.MediaUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher { + + private static final String TAG = Log.tag(DecryptableStreamLocalUriFetcher.class); + + private static final int DIMENSION_LIMIT = 12_000; + + private Context context; + + DecryptableStreamLocalUriFetcher(Context context, Uri uri) { + super(context.getContentResolver(), uri); + this.context = context; + } + + @Override + protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException { + if (MediaUtil.hasVideoThumbnail(context, uri)) { + Bitmap thumbnail = MediaUtil.getVideoThumbnail(context, uri, 1000); + + if (thumbnail != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + thumbnail.compress(Bitmap.CompressFormat.JPEG, 100, baos); + ByteArrayInputStream thumbnailStream = new ByteArrayInputStream(baos.toByteArray()); + thumbnail.recycle(); + return thumbnailStream; + } + if (PartAuthority.isAttachmentUri(uri) && MediaUtil.isVideoType(PartAuthority.getAttachmentContentType(context, uri))) { + try { + AttachmentId attachmentId = PartAuthority.requireAttachmentId(uri); + Uri thumbnailUri = PartAuthority.getAttachmentThumbnailUri(attachmentId); + InputStream thumbStream = PartAuthority.getAttachmentThumbnailStream(context, thumbnailUri); + if (thumbStream != null) { + return thumbStream; + } + } catch (IOException e) { + Log.i(TAG, "Failed to fetch thumbnail", e); + } + } + } + + try { + if (PartAuthority.isBlobUri(uri) && BlobProvider.isSingleUseMemoryBlob(uri)) { + return PartAuthority.getAttachmentThumbnailStream(context, uri); + } else if (isSafeSize(PartAuthority.getAttachmentThumbnailStream(context, uri))) { + return PartAuthority.getAttachmentThumbnailStream(context, uri); + } else { + throw new IOException("File dimensions are too large!"); + } + } catch (IOException ioe) { + Log.w(TAG, ioe); + throw new FileNotFoundException("PartAuthority couldn't load Uri resource."); + } + } + + private boolean isSafeSize(InputStream stream) { + try { + Pair size = BitmapUtil.getDimensions(stream); + return size.first < DIMENSION_LIMIT && size.second < DIMENSION_LIMIT; + } catch (BitmapDecodingException e) { + return false; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamUriLoader.java b/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamUriLoader.java new file mode 100644 index 0000000000..f7b4a6d0b6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/DecryptableStreamUriLoader.java @@ -0,0 +1,55 @@ +package org.thoughtcrime.securesms.glide; + +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 org.thoughtcrime.securesms.mms.DecryptableUri; + +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; + } + + public static class Factory implements ModelLoaderFactory { + + private final Context context; + + public 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/glide/SignalGlideComponents.java b/app/src/main/java/org/thoughtcrime/securesms/glide/SignalGlideComponents.java index e68c0a60d9..c2d81739d8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/glide/SignalGlideComponents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/glide/SignalGlideComponents.java @@ -12,11 +12,12 @@ import com.bumptech.glide.Registry; import com.bumptech.glide.load.model.GlideUrl; import com.bumptech.glide.load.model.UnitModelLoader; import com.bumptech.glide.load.resource.bitmap.BitmapDrawableEncoder; +import com.bumptech.glide.load.resource.bitmap.Downsampler; +import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; import com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.load.resource.gif.StreamGifDecoder; -import org.signal.glide.common.io.InputStreamFactory; import org.signal.glide.load.resource.apng.decode.APNGDecoder; import org.thoughtcrime.securesms.badges.load.BadgeLoader; import org.thoughtcrime.securesms.badges.load.GiftBadgeModel; @@ -36,14 +37,9 @@ import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder; import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder; import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder; import org.thoughtcrime.securesms.glide.cache.EncryptedGifDrawableResourceEncoder; -import org.thoughtcrime.securesms.glide.cache.InputStreamFactoryBitmapDecoder; import org.thoughtcrime.securesms.glide.cache.StreamApngDecoder; -import org.thoughtcrime.securesms.glide.cache.StreamBitmapDecoder; -import org.thoughtcrime.securesms.glide.cache.StreamFactoryApngDecoder; -import org.thoughtcrime.securesms.glide.cache.StreamFactoryGifDecoder; import org.thoughtcrime.securesms.glide.cache.WebpSanDecoder; import org.thoughtcrime.securesms.mms.DecryptableUri; -import org.thoughtcrime.securesms.mms.DecryptableUriStreamLoader; import org.thoughtcrime.securesms.mms.RegisterGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideModule; import org.thoughtcrime.securesms.stickers.StickerRemoteUri; @@ -72,12 +68,10 @@ public class SignalGlideComponents implements RegisterGlideComponents { registry.prepend(InputStream.class, new EncryptedCacheEncoder(secret, glide.getArrayPool())); - registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(context, glide, registry))); + registry.prepend(File.class, Bitmap.class, new EncryptedCacheDecoder<>(secret, new StreamBitmapDecoder(new Downsampler(registry.getImageHeaderParsers(), context.getResources().getDisplayMetrics(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()))); - StreamGifDecoder streamGifDecoder = new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()); - StreamFactoryGifDecoder streamFactoryGifDecoder = new StreamFactoryGifDecoder(streamGifDecoder); + StreamGifDecoder streamGifDecoder = new StreamGifDecoder(registry.getImageHeaderParsers(), new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), glide.getBitmapPool(), glide.getArrayPool()), glide.getArrayPool()); registry.prepend(InputStream.class, GifDrawable.class, streamGifDecoder); - registry.prepend(InputStreamFactory.class, GifDrawable.class, streamFactoryGifDecoder); registry.prepend(GifDrawable.class, new EncryptedGifDrawableResourceEncoder(secret)); registry.prepend(File.class, GifDrawable.class, new EncryptedCacheDecoder<>(secret, streamGifDecoder)); @@ -85,15 +79,13 @@ public class SignalGlideComponents implements RegisterGlideComponents { registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret)); registry.prepend(BitmapDrawable.class, new BitmapDrawableEncoder(glide.getBitmapPool(), encryptedBitmapResourceEncoder)); - ByteBufferApngDecoder byteBufferApngDecoder = new ByteBufferApngDecoder(); - StreamApngDecoder streamApngDecoder = new StreamApngDecoder(byteBufferApngDecoder); - StreamFactoryApngDecoder streamFactoryApngDecoder = new StreamFactoryApngDecoder(byteBufferApngDecoder, glide, registry); + ByteBufferApngDecoder apngBufferCacheDecoder = new ByteBufferApngDecoder(); + StreamApngDecoder apngStreamCacheDecoder = new StreamApngDecoder(apngBufferCacheDecoder); - registry.prepend(InputStream.class, APNGDecoder.class, streamApngDecoder); - registry.prepend(InputStreamFactory.class, APNGDecoder.class, streamFactoryApngDecoder); - registry.prepend(ByteBuffer.class, APNGDecoder.class, byteBufferApngDecoder); + registry.prepend(InputStream.class, APNGDecoder.class, apngStreamCacheDecoder); + registry.prepend(ByteBuffer.class, APNGDecoder.class, apngBufferCacheDecoder); registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret)); - registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, streamApngDecoder)); + registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, apngStreamCacheDecoder)); registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder()); registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder()); @@ -102,8 +94,7 @@ 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, InputStreamFactory.class, new DecryptableUriStreamLoader.Factory(context)); - registry.append(InputStreamFactory.class, Bitmap.class, new InputStreamFactoryBitmapDecoder(context, glide, registry)); + registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context)); 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());