diff --git a/library/src/main/java/com/bumptech/glide/RegistryFactory.java b/library/src/main/java/com/bumptech/glide/RegistryFactory.java index 47fe1d0748..6da62931a8 100644 --- a/library/src/main/java/com/bumptech/glide/RegistryFactory.java +++ b/library/src/main/java/com/bumptech/glide/RegistryFactory.java @@ -56,6 +56,7 @@ import com.bumptech.glide.load.resource.bitmap.ResourceBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder; import com.bumptech.glide.load.resource.bitmap.UnitBitmapDecoder; +import com.bumptech.glide.load.resource.bitmap.UriBitmapImageDecoderResourceDecoder; import com.bumptech.glide.load.resource.bitmap.VideoDecoder; import com.bumptech.glide.load.resource.bytes.ByteBufferRewinder; import com.bumptech.glide.load.resource.drawable.AnimatedImageDecoder; @@ -160,7 +161,8 @@ private static void initializeDefaults( ResourceDecoder byteBufferBitmapDecoder; ResourceDecoder streamBitmapDecoder; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + ResourceDecoder uriBitmapDecoder = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && experiments.isEnabled(EnableImageDecoderForBitmaps.class)) { streamBitmapDecoder = new InputStreamBitmapImageDecoderResourceDecoder( @@ -170,6 +172,7 @@ private static void initializeDefaults( experiments.isEnabled( GlideBuilder.UseArrayPoolForImageDecoderByteBufferAllocation.class)); byteBufferBitmapDecoder = new ByteBufferBitmapImageDecoderResourceDecoder(); + uriBitmapDecoder = new UriBitmapImageDecoderResourceDecoder(context); } else { byteBufferBitmapDecoder = new ByteBufferBitmapDecoder(downsampler); streamBitmapDecoder = new StreamBitmapDecoder(downsampler, arrayPool); @@ -204,6 +207,11 @@ private static void initializeDefaults( .append(Registry.BUCKET_BITMAP, ByteBuffer.class, Bitmap.class, byteBufferBitmapDecoder) .append(Registry.BUCKET_BITMAP, InputStream.class, Bitmap.class, streamBitmapDecoder); + if (uriBitmapDecoder != null) { + registry.prepend(Uri.class, Bitmap.class, uriBitmapDecoder); + registry.prepend(Uri.class, Uri.class, UnitModelLoader.Factory.getInstance()); + } + if (ParcelFileDescriptorRewinder.isSupported()) { registry.append( Registry.BUCKET_BITMAP, @@ -244,7 +252,16 @@ private static void initializeDefaults( Registry.BUCKET_BITMAP_DRAWABLE, ParcelFileDescriptor.class, BitmapDrawable.class, - new BitmapDrawableDecoder<>(resources, parcelFileDescriptorVideoDecoder)) + new BitmapDrawableDecoder<>(resources, parcelFileDescriptorVideoDecoder)); + + if (uriBitmapDecoder != null) { + registry.prepend( + Uri.class, + BitmapDrawable.class, + new BitmapDrawableDecoder<>(resources, uriBitmapDecoder)); + } + + registry .append(BitmapDrawable.class, new BitmapDrawableEncoder(bitmapPool, bitmapEncoder)) /* GIFs */ .append( diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/UriBitmapImageDecoderResourceDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/UriBitmapImageDecoderResourceDecoder.java new file mode 100644 index 0000000000..3191d9228e --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/UriBitmapImageDecoderResourceDecoder.java @@ -0,0 +1,57 @@ +package com.bumptech.glide.load.resource.bitmap; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.ImageDecoder; +import android.graphics.ImageDecoder.Source; +import android.net.Uri; +import android.os.Build; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; +import java.io.IOException; + +/** Decodes {@link Bitmap}s from {@link Uri}s using {@link ImageDecoder}. */ +@RequiresApi(Build.VERSION_CODES.P) +public final class UriBitmapImageDecoderResourceDecoder implements ResourceDecoder { + private static final String TAG = "UriBitmapDecoder"; + private final Context context; + private final BitmapImageDecoderResourceDecoder wrapped = new BitmapImageDecoderResourceDecoder(); + + public UriBitmapImageDecoderResourceDecoder(@NonNull Context context) { + this.context = context.getApplicationContext(); + } + + @Override + public boolean handles(@NonNull Uri uri, @NonNull Options options) throws IOException { + String scheme = uri.getScheme(); + boolean isSupportedScheme = + ContentResolver.SCHEME_CONTENT.equals(scheme) + || ContentResolver.SCHEME_FILE.equals(scheme) + || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme); + if (!isSupportedScheme) { + return false; + } + String mimeType = context.getContentResolver().getType(uri); + if (mimeType != null && mimeType.equals("image/gif")) { + return false; + } + return true; + } + + @Override + public Resource decode(@NonNull Uri uri, int width, int height, @NonNull Options options) + throws IOException { + Source source = ImageDecoder.createSource(context.getContentResolver(), uri); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + String mimeType = context.getContentResolver().getType(uri); + Log.v( + TAG, "decoding " + uri + ", mimeType: " + mimeType + ", [" + width + ", " + height + "]"); + } + return wrapped.decode(source, width, height, options); + } +} diff --git a/library/src/test/java/com/bumptech/glide/load/resource/bitmap/UriBitmapImageDecoderResourceDecoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/bitmap/UriBitmapImageDecoderResourceDecoderTest.java new file mode 100644 index 0000000000..f005d3e5a2 --- /dev/null +++ b/library/src/test/java/com/bumptech/glide/load/resource/bitmap/UriBitmapImageDecoderResourceDecoderTest.java @@ -0,0 +1,77 @@ +package com.bumptech.glide.load.resource.bitmap; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.bumptech.glide.load.Options; +import java.io.IOException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +@RunWith(AndroidJUnit4.class) +@Config(sdk = Build.VERSION_CODES.Q) +public final class UriBitmapImageDecoderResourceDecoderTest { + + private Context context; + private UriBitmapImageDecoderResourceDecoder decoder; + private Options options; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + decoder = new UriBitmapImageDecoderResourceDecoder(context); + options = new Options(); + } + + @Test + public void handles_returnsTrueForUri() throws IOException { + ContentValues values = new ContentValues(); + values.put(MediaStore.Images.Media.MIME_TYPE, "image/png"); + Uri uri = + context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + assertThat(decoder.handles(uri, options)).isTrue(); + } + + @Test + public void decode_withNonExistentUri_throwsIOException() { + Uri uri = Uri.parse("file:///non-existent-file.png"); + assertThrows( + IOException.class, () -> decoder.decode(uri, /* width= */ 100, /* height= */ 100, options)); + } + + @Test + public void handles_returnsTrueForFileUri() throws IOException { + Uri uri = Uri.parse("file:///path/to/image.png"); + assertThat(decoder.handles(uri, options)).isTrue(); + } + + @Test + public void handles_returnsTrueForResourceUri() throws IOException { + Uri uri = Uri.parse("android.resource://com.bumptech.glide.test/raw/image"); + assertThat(decoder.handles(uri, options)).isTrue(); + } + + @Test + public void handles_returnsFalseForHttpUri() throws IOException { + Uri uri = Uri.parse("http://example.com/image.png"); + assertThat(decoder.handles(uri, options)).isFalse(); + } + + @Test + public void handles_returnsFalseForGifUri() throws IOException { + ContentValues values = new ContentValues(); + values.put(MediaStore.Images.Media.MIME_TYPE, "image/gif"); + Uri uri = + context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + assertThat(decoder.handles(uri, options)).isFalse(); + } +} diff --git a/library/test/src/test/java/com/bumptech/glide/RegistryFactoryTest.java b/library/test/src/test/java/com/bumptech/glide/RegistryFactoryTest.java index 6fb7077b41..4bff652967 100644 --- a/library/test/src/test/java/com/bumptech/glide/RegistryFactoryTest.java +++ b/library/test/src/test/java/com/bumptech/glide/RegistryFactoryTest.java @@ -1,21 +1,39 @@ package com.bumptech.glide; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; import android.content.Context; +import android.content.pm.ProviderInfo; +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.module.AppGlideModule; import com.bumptech.glide.tests.TearDownGlide; import com.bumptech.glide.util.GlideSuppliers.GlideSupplier; import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.junit.Rule; import org.junit.Test; import org.junit.function.ThrowingRunnable; import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.annotation.Config; @RunWith(AndroidJUnit4.class) +@Config(sdk = Build.VERSION_CODES.Q) public class RegistryFactoryTest { @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); private final Context context = ApplicationProvider.getApplicationContext(); @@ -58,4 +76,172 @@ public void run() { } }); } + + @Test + public void lazilyCreate_whenImageDecoderEnabled_localGifLoadsAsAnimated() throws Exception { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return; + } + GlideBuilder builder = new GlideBuilder(); + builder.setImageDecoderEnabledForBitmaps(true); + Glide.init(context, builder); + + Uri gifUri = Uri.parse("content://com.bumptech.glide.test/test.gif"); + ProviderInfo info = new ProviderInfo(); + info.authority = "com.bumptech.glide.test"; + Robolectric.buildContentProvider(FakeContentProvider.class).create(info); + + // Load the GIF as a Drawable on a background thread. + Future future = + Executors.newSingleThreadExecutor() + .submit(() -> Glide.with(context).asDrawable().load(gifUri).submit().get()); + + Drawable drawable = future.get(5, TimeUnit.SECONDS); + + assertThat(drawable).isInstanceOf(GifDrawable.class); + } + + @Test + public void testImageDecoderDecodesTinyGif() throws Exception { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return; + } + Uri gifUri = Uri.parse("content://com.bumptech.glide.test/test.gif"); + ProviderInfo info = new ProviderInfo(); + info.authority = "com.bumptech.glide.test"; + Robolectric.buildContentProvider(FakeContentProvider.class).create(info); + + android.graphics.ImageDecoder.Source source = + android.graphics.ImageDecoder.createSource(context.getContentResolver(), gifUri); + try { + Bitmap bitmap = android.graphics.ImageDecoder.decodeBitmap(source); + System.out.println("ImageDecoder succeeded: " + bitmap); + } catch (Throwable t) { + System.out.println("ImageDecoder failed!"); + t.printStackTrace(); + throw new RuntimeException(t); + } + } + + private static class FakeContentProvider extends android.content.ContentProvider { + private static final byte[] TINY_GIF = + new byte[] { + 0x47, + 0x49, + 0x46, + 0x38, + 0x39, + 0x61, + 0x01, + 0x00, + 0x01, + 0x00, + (byte) 0x80, + 0x00, + 0x00, + (byte) 0xff, + (byte) 0xff, + (byte) 0xff, + 0x00, + 0x00, + 0x00, + 0x21, + (byte) 0xf9, + 0x04, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x2c, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x01, + 0x00, + 0x00, + 0x02, + 0x02, + 0x44, + 0x01, + 0x00, + 0x3b + }; + + @Override + public boolean onCreate() { + return true; + } + + @Override + public android.database.Cursor query( + @NonNull Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + return null; + } + + @Override + public String getType(@NonNull Uri uri) { + return "image/gif"; + } + + @Override + public Uri insert(@NonNull Uri uri, android.content.ContentValues values) { + return null; + } + + @Override + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update( + @NonNull Uri uri, + android.content.ContentValues values, + String selection, + String[] selectionArgs) { + return 0; + } + + @Override + public android.content.res.AssetFileDescriptor openTypedAssetFile( + @NonNull Uri uri, @NonNull String mimeTypeFilter, @Nullable Bundle opts) + throws java.io.FileNotFoundException { + if (mimeTypeFilter.contains("gif") + || mimeTypeFilter.equals("*/*") + || mimeTypeFilter.equals("image/*")) { + ParcelFileDescriptor pfd = + openPipeHelper( + uri, + mimeTypeFilter, + opts, + TINY_GIF, + new PipeDataWriter() { + @Override + public void writeDataToPipe( + @NonNull ParcelFileDescriptor output, + @NonNull Uri uri, + @NonNull String mimeType, + @Nullable Bundle opts, + @Nullable byte[] args) { + try (java.io.FileOutputStream out = + new java.io.FileOutputStream(output.getFileDescriptor())) { + out.write(args); + } catch (IOException e) { + // ignore + } + } + }); + return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); + } + return super.openTypedAssetFile(uri, mimeTypeFilter, opts); + } + } }