diff --git a/instrumentation/src/androidTest/java/com/bumptech/glide/AsBytesTest.java b/instrumentation/src/androidTest/java/com/bumptech/glide/AsBytesTest.java index c4f8761408..24a4951b16 100644 --- a/instrumentation/src/androidTest/java/com/bumptech/glide/AsBytesTest.java +++ b/instrumentation/src/androidTest/java/com/bumptech/glide/AsBytesTest.java @@ -84,8 +84,6 @@ public void loadBitmapDrawable_asBytes_providesBytesOfBitmap() { assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } - // TODO: This should pass. - @Ignore @Test public void loadVideoResourceId_asBytes_providesBytesOfFrame() { byte[] data = @@ -99,8 +97,6 @@ public void loadVideoResourceId_asBytes_providesBytesOfFrame() { assertThat(BitmapFactory.decodeByteArray(data, 0, data.length)).isNotNull(); } - // TODO: This should pass. - @Ignore @Test public void loadVideoResourceId_asBytes_withFrameTime_providesBytesOfFrame() { byte[] data = diff --git a/instrumentation/src/androidTest/java/com/bumptech/glide/LoadVideoResourceTest.java b/instrumentation/src/androidTest/java/com/bumptech/glide/LoadVideoResourceTest.java new file mode 100644 index 0000000000..d99570490b --- /dev/null +++ b/instrumentation/src/androidTest/java/com/bumptech/glide/LoadVideoResourceTest.java @@ -0,0 +1,323 @@ +package com.bumptech.glide; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import com.bumptech.glide.test.ConcurrencyHelper; +import com.bumptech.glide.test.GlideApp; +import com.bumptech.glide.test.ResourceIds; +import com.bumptech.glide.test.TearDownGlide; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; + +/** + * Tests that Glide is able to load videos stored in resources and loaded as + * {@link android.content.res.AssetFileDescriptor}s. + */ +@RunWith(AndroidJUnit4.class) +public class LoadVideoResourceTest { + @Rule public final TearDownGlide tearDownGlide = new TearDownGlide(); + private final ConcurrencyHelper concurrency = new ConcurrencyHelper(); + + private Context context; + + @Before + public void setUp() throws IOException { + MockitoAnnotations.initMocks(this); + context = InstrumentationRegistry.getTargetContext(); + } + + @Test + public void loadVideoResourceId_fromInt_decodesFrame() { + Drawable frame = + concurrency.get( + Glide.with(context) + .load(ResourceIds.raw.video) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceId_fromInt_withFrameTime_decodesFrame() { + Drawable frame = + concurrency.get( + GlideApp.with(context) + .load(ResourceIds.raw.video) + .frame(TimeUnit.SECONDS.toMicros(1)) + .submit()); + + assertThat(frame).isNotNull(); + } + + // Testing boxed integer. + @SuppressWarnings("UnnecessaryBoxing") + @Test + public void loadVideoResourceId_fromInteger_decodesFrame() { + Drawable frame = + concurrency.get( + Glide.with(context) + .load(new Integer(ResourceIds.raw.video)) + .submit()); + + assertThat(frame).isNotNull(); + } + + // Testing boxed integer. + @SuppressWarnings("UnnecessaryBoxing") + @Test + public void loadVideoResourceId_fromInteger_withFrameTime_decodesFrame() { + Drawable frame = + concurrency.get( + GlideApp.with(context) + .load(new Integer(ResourceIds.raw.video)) + .frame(TimeUnit.SECONDS.toMicros(1)) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceId_asBitmap_decodesFrame() { + Bitmap frame = + concurrency.get( + Glide.with(context) + .asBitmap() + .load(ResourceIds.raw.video) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceId_asBitmap_withFrameTime_decodesFrame() { + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(ResourceIds.raw.video) + .frame(TimeUnit.SECONDS.toMicros(1)) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUri_fromId_decodesFrame() { + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(context.getPackageName()) + .path(String.valueOf(ResourceIds.raw.video)) + .build(); + + Drawable frame = + concurrency.get( + GlideApp.with(context) + .load(uri) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUri_asBitmap_fromId_decodesFrame() { + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(context.getPackageName()) + .path(String.valueOf(ResourceIds.raw.video)) + .build(); + + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(uri) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUri_fromId_withFrame_decodesFrame() { + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(context.getPackageName()) + .path(String.valueOf(ResourceIds.raw.video)) + .build(); + + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(uri) + .frame(TimeUnit.SECONDS.toMicros(1)) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUriString_fromId_decodesFrame() { + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(context.getPackageName()) + .path(String.valueOf(ResourceIds.raw.video)) + .build(); + + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(uri.toString()) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUriString_fromId_withFrame_decodesFrame() { + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(context.getPackageName()) + .path(String.valueOf(ResourceIds.raw.video)) + .build(); + + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(uri.toString()) + .frame(TimeUnit.SECONDS.toMicros(1)) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUri_fromName_decodesFrame() { + Resources resources = context.getResources(); + int resourceId = ResourceIds.raw.video; + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(resourceId)) + .appendPath(resources.getResourceTypeName(resourceId)) + .appendPath(resources.getResourceEntryName(resourceId)) + .build(); + + Drawable frame = + concurrency.get( + GlideApp.with(context) + .load(uri) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUri_asBitmap_fromName_decodesFrame() { + Resources resources = context.getResources(); + int resourceId = ResourceIds.raw.video; + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(resourceId)) + .appendPath(resources.getResourceTypeName(resourceId)) + .appendPath(resources.getResourceEntryName(resourceId)) + .build(); + + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(uri) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUri_fromName_withFrame_decodesFrame() { + Resources resources = context.getResources(); + int resourceId = ResourceIds.raw.video; + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(resourceId)) + .appendPath(resources.getResourceTypeName(resourceId)) + .appendPath(resources.getResourceEntryName(resourceId)) + .build(); + + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(uri) + .frame(TimeUnit.SECONDS.toMicros(1)) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUriString_fromName_decodesFrame() { + Resources resources = context.getResources(); + int resourceId = ResourceIds.raw.video; + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(resourceId)) + .appendPath(resources.getResourceTypeName(resourceId)) + .appendPath(resources.getResourceEntryName(resourceId)) + .build(); + + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(uri.toString()) + .submit()); + + assertThat(frame).isNotNull(); + } + + @Test + public void loadVideoResourceUriString_fromName_withFrame_decodesFrame() { + Resources resources = context.getResources(); + int resourceId = ResourceIds.raw.video; + Uri uri = + new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(resources.getResourcePackageName(resourceId)) + .appendPath(resources.getResourceTypeName(resourceId)) + .appendPath(resources.getResourceEntryName(resourceId)) + .build(); + + Bitmap frame = + concurrency.get( + GlideApp.with(context) + .asBitmap() + .load(uri.toString()) + .frame(TimeUnit.SECONDS.toMicros(1)) + .submit()); + + assertThat(frame).isNotNull(); + } +} diff --git a/library/src/main/java/com/bumptech/glide/Glide.java b/library/src/main/java/com/bumptech/glide/Glide.java index 190ca1b71e..56db4b0248 100644 --- a/library/src/main/java/com/bumptech/glide/Glide.java +++ b/library/src/main/java/com/bumptech/glide/Glide.java @@ -2,7 +2,9 @@ import android.app.Activity; import android.content.ComponentCallbacks2; +import android.content.ContentResolver; import android.content.Context; +import android.content.res.AssetFileDescriptor; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; @@ -19,6 +21,7 @@ import android.view.View; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.DecodeFormat; +import com.bumptech.glide.load.ResourceDecoder; import com.bumptech.glide.load.data.InputStreamRewinder; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool; @@ -54,7 +57,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.VideoBitmapDecoder; +import com.bumptech.glide.load.resource.bitmap.VideoDecoder; import com.bumptech.glide.load.resource.bytes.ByteBufferRewinder; import com.bumptech.glide.load.resource.drawable.ResourceDrawableDecoder; import com.bumptech.glide.load.resource.drawable.UnitDrawableDecoder; @@ -333,7 +336,8 @@ private static void throwIncorrectGlideModule(Exception e) { resources.getDisplayMetrics(), bitmapPool, arrayPool); ByteBufferGifDecoder byteBufferGifDecoder = new ByteBufferGifDecoder(context, registry.getImageHeaderParsers(), bitmapPool, arrayPool); - VideoBitmapDecoder videoBitmapDecoder = new VideoBitmapDecoder(bitmapPool); + ResourceDecoder parcelFileDescriptorVideoDecoder = + VideoDecoder.parcel(bitmapPool); ByteBufferBitmapDecoder byteBufferBitmapDecoder = new ByteBufferBitmapDecoder(downsampler); StreamBitmapDecoder streamBitmapDecoder = new StreamBitmapDecoder(downsampler, arrayPool); ResourceDrawableDecoder resourceDrawableDecoder = @@ -344,8 +348,12 @@ private static void throwIncorrectGlideModule(Exception e) { new ResourceLoader.UriFactory(resources); ResourceLoader.FileDescriptorFactory resourceLoaderFileDescriptorFactory = new ResourceLoader.FileDescriptorFactory(resources); + ResourceLoader.AssetFileDescriptorFactory resourceLoaderAssetFileDescriptorFactory = + new ResourceLoader.AssetFileDescriptorFactory(resources); BitmapEncoder bitmapEncoder = new BitmapEncoder(); + ContentResolver contentResolver = context.getContentResolver(); + registry .append(ByteBuffer.class, new ByteBufferEncoder()) .append(InputStream.class, new StreamEncoder(arrayPool)) @@ -353,10 +361,18 @@ private static void throwIncorrectGlideModule(Exception e) { .append(Registry.BUCKET_BITMAP, ByteBuffer.class, Bitmap.class, byteBufferBitmapDecoder) .append(Registry.BUCKET_BITMAP, InputStream.class, Bitmap.class, streamBitmapDecoder) .append( - Registry.BUCKET_BITMAP, ParcelFileDescriptor.class, Bitmap.class, videoBitmapDecoder) + Registry.BUCKET_BITMAP, + ParcelFileDescriptor.class, + Bitmap.class, + parcelFileDescriptorVideoDecoder) .append( - Registry.BUCKET_BITMAP, Bitmap.class, Bitmap.class, new UnitBitmapDecoder()) + Registry.BUCKET_BITMAP, + AssetFileDescriptor.class, + Bitmap.class, + VideoDecoder.asset(bitmapPool)) .append(Bitmap.class, Bitmap.class, UnitModelLoader.Factory.getInstance()) + .append( + Registry.BUCKET_BITMAP, Bitmap.class, Bitmap.class, new UnitBitmapDecoder()) .append(Bitmap.class, bitmapEncoder) /* BitmapDrawables */ .append( @@ -373,7 +389,7 @@ Registry.BUCKET_BITMAP, Bitmap.class, Bitmap.class, new UnitBitmapDecoder()) Registry.BUCKET_BITMAP_DRAWABLE, ParcelFileDescriptor.class, BitmapDrawable.class, - new BitmapDrawableDecoder<>(resources, videoBitmapDecoder)) + new BitmapDrawableDecoder<>(resources, parcelFileDescriptorVideoDecoder)) .append(BitmapDrawable.class, new BitmapDrawableEncoder(bitmapPool, bitmapEncoder)) /* GIFs */ .append( @@ -408,33 +424,49 @@ Uri.class, Bitmap.class, new ResourceBitmapDecoder(resourceDrawableDecoder, bitm .register(new InputStreamRewinder.Factory(arrayPool)) .append(int.class, InputStream.class, resourceLoaderStreamFactory) .append( - int.class, - ParcelFileDescriptor.class, - resourceLoaderFileDescriptorFactory) + int.class, + ParcelFileDescriptor.class, + resourceLoaderFileDescriptorFactory) .append(Integer.class, InputStream.class, resourceLoaderStreamFactory) .append( - Integer.class, - ParcelFileDescriptor.class, - resourceLoaderFileDescriptorFactory) + Integer.class, + ParcelFileDescriptor.class, + resourceLoaderFileDescriptorFactory) .append(Integer.class, Uri.class, resourceLoaderUriFactory) + .append( + int.class, + AssetFileDescriptor.class, + resourceLoaderAssetFileDescriptorFactory) + .append( + Integer.class, + AssetFileDescriptor.class, + resourceLoaderAssetFileDescriptorFactory) .append(int.class, Uri.class, resourceLoaderUriFactory) .append(String.class, InputStream.class, new DataUrlLoader.StreamFactory()) .append(String.class, InputStream.class, new StringLoader.StreamFactory()) .append(String.class, ParcelFileDescriptor.class, new StringLoader.FileDescriptorFactory()) + .append( + String.class, AssetFileDescriptor.class, new StringLoader.AssetFileDescriptorFactory()) .append(Uri.class, InputStream.class, new HttpUriLoader.Factory()) .append(Uri.class, InputStream.class, new AssetUriLoader.StreamFactory(context.getAssets())) .append( - Uri.class, - ParcelFileDescriptor.class, - new AssetUriLoader.FileDescriptorFactory(context.getAssets())) + Uri.class, + ParcelFileDescriptor.class, + new AssetUriLoader.FileDescriptorFactory(context.getAssets())) .append(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context)) .append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context)) .append( Uri.class, InputStream.class, - new UriLoader.StreamFactory(context.getContentResolver())) - .append(Uri.class, ParcelFileDescriptor.class, - new UriLoader.FileDescriptorFactory(context.getContentResolver())) + new UriLoader.StreamFactory(contentResolver)) + .append( + Uri.class, + ParcelFileDescriptor.class, + new UriLoader.FileDescriptorFactory(contentResolver)) + .append( + Uri.class, + AssetFileDescriptor.class, + new UriLoader.AssetFileDescriptorFactory(contentResolver)) .append(Uri.class, InputStream.class, new UrlUriLoader.StreamFactory()) .append(URL.class, InputStream.class, new UrlLoader.StreamFactory()) .append(Uri.class, File.class, new MediaStoreFileLoader.Factory(context)) diff --git a/library/src/main/java/com/bumptech/glide/load/data/AssetFileDescriptorLocalUriFetcher.java b/library/src/main/java/com/bumptech/glide/load/data/AssetFileDescriptorLocalUriFetcher.java new file mode 100644 index 0000000000..66a287823a --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/data/AssetFileDescriptorLocalUriFetcher.java @@ -0,0 +1,39 @@ +package com.bumptech.glide.load.data; + +import android.content.ContentResolver; +import android.content.res.AssetFileDescriptor; +import android.net.Uri; +import android.support.annotation.NonNull; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * Fetches an {@link AssetFileDescriptor} for a local {@link android.net.Uri}. + */ +public final class AssetFileDescriptorLocalUriFetcher extends LocalUriFetcher { + + public AssetFileDescriptorLocalUriFetcher(ContentResolver contentResolver, Uri uri) { + super(contentResolver, uri); + } + + @Override + protected AssetFileDescriptor loadResource(Uri uri, ContentResolver contentResolver) + throws FileNotFoundException { + AssetFileDescriptor result = contentResolver.openAssetFileDescriptor(uri, "r"); + if (result == null) { + throw new FileNotFoundException("FileDescriptor is null for: " + uri); + } + return result; + } + + @Override + protected void close(AssetFileDescriptor data) throws IOException { + data.close(); + } + + @NonNull + @Override + public Class getDataClass() { + return AssetFileDescriptor.class; + } +} diff --git a/library/src/main/java/com/bumptech/glide/load/model/ResourceLoader.java b/library/src/main/java/com/bumptech/glide/load/model/ResourceLoader.java index 5ede18bb80..1abc1fab4d 100644 --- a/library/src/main/java/com/bumptech/glide/load/model/ResourceLoader.java +++ b/library/src/main/java/com/bumptech/glide/load/model/ResourceLoader.java @@ -1,6 +1,7 @@ package com.bumptech.glide.load.model; import android.content.ContentResolver; +import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.net.Uri; import android.os.ParcelFileDescriptor; @@ -104,6 +105,30 @@ public void teardown() { } } + /** + * Loads {@link AssetFileDescriptor}s from resource ids. + */ + public static final class AssetFileDescriptorFactory + implements ModelLoaderFactory { + + private final Resources resources; + + public AssetFileDescriptorFactory(Resources resources) { + this.resources = resources; + } + + @Override + public ModelLoader build(MultiModelLoaderFactory multiFactory) { + return new ResourceLoader<>( + resources, multiFactory.build(Uri.class, AssetFileDescriptor.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } + /** * Factory for loading resource {@link Uri}s from Android resource ids. */ diff --git a/library/src/main/java/com/bumptech/glide/load/model/StringLoader.java b/library/src/main/java/com/bumptech/glide/load/model/StringLoader.java index 26e2c95ecf..9ce67c102d 100644 --- a/library/src/main/java/com/bumptech/glide/load/model/StringLoader.java +++ b/library/src/main/java/com/bumptech/glide/load/model/StringLoader.java @@ -1,5 +1,6 @@ package com.bumptech.glide.load.model; +import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; @@ -91,4 +92,21 @@ public void teardown() { // Do nothing. } } + + /** + * Loads {@link AssetFileDescriptor}s from Strings. + */ + public static final class AssetFileDescriptorFactory + implements ModelLoaderFactory { + + @Override + public ModelLoader build(MultiModelLoaderFactory multiFactory) { + return new StringLoader<>(multiFactory.build(Uri.class, AssetFileDescriptor.class)); + } + + @Override + public void teardown() { + // Do nothing. + } + } } diff --git a/library/src/main/java/com/bumptech/glide/load/model/UriLoader.java b/library/src/main/java/com/bumptech/glide/load/model/UriLoader.java index 3b02a4902c..77a316dd4c 100644 --- a/library/src/main/java/com/bumptech/glide/load/model/UriLoader.java +++ b/library/src/main/java/com/bumptech/glide/load/model/UriLoader.java @@ -1,10 +1,12 @@ package com.bumptech.glide.load.model; import android.content.ContentResolver; +import android.content.res.AssetFileDescriptor; import android.net.Uri; import android.os.ParcelFileDescriptor; import android.support.annotation.NonNull; import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.data.AssetFileDescriptorLocalUriFetcher; import com.bumptech.glide.load.data.DataFetcher; import com.bumptech.glide.load.data.FileDescriptorLocalUriFetcher; import com.bumptech.glide.load.data.StreamLocalUriFetcher; @@ -94,8 +96,8 @@ public void teardown() { /** * Loads {@link ParcelFileDescriptor}s from {@link Uri}s. */ - public static class FileDescriptorFactory implements ModelLoaderFactory, + public static class FileDescriptorFactory + implements ModelLoaderFactory, LocalUriFetcherFactory { private final ContentResolver contentResolver; @@ -120,4 +122,33 @@ public void teardown() { // Do nothing. } } + + /** + * Loads {@link AssetFileDescriptor}s from {@link Uri}s. + */ + public static final class AssetFileDescriptorFactory + implements ModelLoaderFactory, + LocalUriFetcherFactory { + + private final ContentResolver contentResolver; + + public AssetFileDescriptorFactory(ContentResolver contentResolver) { + this.contentResolver = contentResolver; + } + + @Override + public ModelLoader build(MultiModelLoaderFactory multiFactory) { + return new UriLoader<>(this); + } + + @Override + public void teardown() { + // Do nothing. + } + + @Override + public DataFetcher build(Uri uri) { + return new AssetFileDescriptorLocalUriFetcher(contentResolver, uri); + } + } } diff --git a/library/src/main/java/com/bumptech/glide/load/model/stream/MediaStoreVideoThumbLoader.java b/library/src/main/java/com/bumptech/glide/load/model/stream/MediaStoreVideoThumbLoader.java index 678961ed0e..0685decb15 100644 --- a/library/src/main/java/com/bumptech/glide/load/model/stream/MediaStoreVideoThumbLoader.java +++ b/library/src/main/java/com/bumptech/glide/load/model/stream/MediaStoreVideoThumbLoader.java @@ -10,7 +10,7 @@ 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.load.resource.bitmap.VideoBitmapDecoder; +import com.bumptech.glide.load.resource.bitmap.VideoDecoder; import com.bumptech.glide.signature.ObjectKey; import java.io.InputStream; @@ -18,8 +18,8 @@ * Loads {@link InputStream}s from media store video {@link Uri}s that point to pre-generated * thumbnails for those {@link Uri}s in the media store. * - *

If {@link VideoBitmapDecoder#TARGET_FRAME} is set with a non-null value that is not equal to - * {@link VideoBitmapDecoder#DEFAULT_FRAME}, this loader will always return {@code null}. The media + *

If {@link VideoDecoder#TARGET_FRAME} is set with a non-null value that is not equal to + * {@link VideoDecoder#DEFAULT_FRAME}, this loader will always return {@code null}. The media * store does not use a defined frame to generate the thumbnail, so we cannot accurately fulfill * requests for specific frames. */ @@ -44,8 +44,8 @@ public LoadData buildLoadData(@NonNull Uri model, int width, int he } private boolean isRequestingDefaultFrame(Options options) { - Long specifiedFrame = options.get(VideoBitmapDecoder.TARGET_FRAME); - return specifiedFrame != null && specifiedFrame == VideoBitmapDecoder.DEFAULT_FRAME; + Long specifiedFrame = options.get(VideoDecoder.TARGET_FRAME); + return specifiedFrame != null && specifiedFrame == VideoDecoder.DEFAULT_FRAME; } @Override diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java index ce47e2f6bd..226765e2d3 100644 --- a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoBitmapDecoder.java @@ -1,20 +1,9 @@ package com.bumptech.glide.load.resource.bitmap; import android.content.Context; -import android.graphics.Bitmap; -import android.media.MediaMetadataRetriever; import android.os.ParcelFileDescriptor; -import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; import com.bumptech.glide.Glide; -import com.bumptech.glide.load.Option; -import com.bumptech.glide.load.Options; -import com.bumptech.glide.load.ResourceDecoder; -import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.security.MessageDigest; /** * An {@link com.bumptech.glide.load.ResourceDecoder} that can decode a thumbnail frame @@ -22,134 +11,21 @@ * video. * * @see android.media.MediaMetadataRetriever + * + * @deprecated Use {@link VideoDecoder#parcel(BitmapPool)} instead. This class may be removed and + * {@link VideoDecoder} may become final in a future version of Glide. */ -public class VideoBitmapDecoder implements ResourceDecoder { - /** - * A constant indicating we should use whatever frame we consider best, frequently not the first - * frame. - */ - public static final long DEFAULT_FRAME = -1; - - /** - * A long indicating the time position (in microseconds) of the target frame which will be - * retrieved. {@link android.media.MediaMetadataRetriever#getFrameAtTime(long)} is used to - * extract the video frame. - * - *

When retrieving the frame at the given time position, there is no guarantee that the data - * source has a frame located at the position. When this happens, a frame nearby will be returned. - * If the long is negative, time position and option will ignored, and any frame that the - * implementation considers as representative may be returned. - */ - public static final Option TARGET_FRAME = Option.disk( - "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.TargetFrame", DEFAULT_FRAME, - new Option.CacheKeyUpdater() { - private final ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); - - @Override - public void update(byte[] keyBytes, Long value, MessageDigest messageDigest) { - messageDigest.update(keyBytes); - synchronized (buffer) { - buffer.position(0); - messageDigest.update(buffer.putLong(value).array()); - } - } - }); - - /** - * An integer indicating the frame option used to retrieve a target frame. - * - *

This option will be ignored if {@link #TARGET_FRAME} is not set or is set to - * {@link #DEFAULT_FRAME}. - * - * @see MediaMetadataRetriever#getFrameAtTime(long, int) - */ - // Public API. - @SuppressWarnings("WeakerAccess") - public static final Option FRAME_OPTION = Option.disk( - "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.FrameOption", - null /*defaultValue*/, - new Option.CacheKeyUpdater() { - private final ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE); +@Deprecated +public class VideoBitmapDecoder extends VideoDecoder { - @Override - public void update(byte[] keyBytes, Integer value, MessageDigest messageDigest) { - if (value == null) { - return; - } - messageDigest.update(keyBytes); - synchronized (buffer) { - buffer.position(0); - messageDigest.update(buffer.putInt(value).array()); - } - } - } - ); - - private static final MediaMetadataRetrieverFactory DEFAULT_FACTORY = - new MediaMetadataRetrieverFactory(); - - private final BitmapPool bitmapPool; - private final MediaMetadataRetrieverFactory factory; - - // Public API. @SuppressWarnings("unused") public VideoBitmapDecoder(Context context) { this(Glide.get(context).getBitmapPool()); } + // Public API + @SuppressWarnings("WeakerAccess") public VideoBitmapDecoder(BitmapPool bitmapPool) { - this(bitmapPool, DEFAULT_FACTORY); - } - - @VisibleForTesting - VideoBitmapDecoder(BitmapPool bitmapPool, MediaMetadataRetrieverFactory factory) { - this.bitmapPool = bitmapPool; - this.factory = factory; - } - - @Override - public boolean handles(@NonNull ParcelFileDescriptor data, @NonNull Options options) { - // Calling setDataSource is expensive so avoid doing so unless we're actually called. - // For non-videos this isn't any cheaper, but for videos it saves the redundant call and - // 50-100ms. - return true; - } - - @Override - public Resource decode(@NonNull ParcelFileDescriptor resource, int outWidth, - int outHeight, @NonNull Options options) throws IOException { - long frameTimeMicros = options.get(TARGET_FRAME); - if (frameTimeMicros < 0 && frameTimeMicros != DEFAULT_FRAME) { - throw new IllegalArgumentException( - "Requested frame must be non-negative, or DEFAULT_FRAME, given: " + frameTimeMicros); - } - Integer frameOption = options.get(FRAME_OPTION); - - final Bitmap result; - MediaMetadataRetriever mediaMetadataRetriever = factory.build(); - try { - mediaMetadataRetriever.setDataSource(resource.getFileDescriptor()); - if (frameTimeMicros == DEFAULT_FRAME) { - result = mediaMetadataRetriever.getFrameAtTime(); - } else if (frameOption == null) { - result = mediaMetadataRetriever.getFrameAtTime(frameTimeMicros); - } else { - result = mediaMetadataRetriever.getFrameAtTime(frameTimeMicros, frameOption); - } - } catch (RuntimeException e) { - // MediaMetadataRetriever APIs throw generic runtime exceptions when given invalid data. - throw new IOException(e); - } finally { - mediaMetadataRetriever.release(); - } - resource.close(); - return BitmapResource.obtain(result, bitmapPool); - } - - @VisibleForTesting - static class MediaMetadataRetrieverFactory { - public MediaMetadataRetriever build() { - return new MediaMetadataRetriever(); - } + super(bitmapPool, new ParcelFileDescriptorInitializer()); } } diff --git a/library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoDecoder.java b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoDecoder.java new file mode 100644 index 0000000000..f1b7c32d59 --- /dev/null +++ b/library/src/main/java/com/bumptech/glide/load/resource/bitmap/VideoDecoder.java @@ -0,0 +1,184 @@ +package com.bumptech.glide.load.resource.bitmap; + +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.media.MediaMetadataRetriever; +import android.os.ParcelFileDescriptor; +import android.support.annotation.VisibleForTesting; +import com.bumptech.glide.load.Option; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.MessageDigest; + + +/** + * Decodes video data to Bitmaps from {@link ParcelFileDescriptor}s and + * {@link AssetFileDescriptor}s. + * + * @param The type of data, currently either {@link ParcelFileDescriptor} or + * {@link AssetFileDescriptor}. + */ +public class VideoDecoder implements ResourceDecoder { + + /** + * A constant indicating we should use whatever frame we consider best, frequently not the first + * frame. + */ + public static final long DEFAULT_FRAME = -1; + + /** + * A long indicating the time position (in microseconds) of the target frame which will be + * retrieved. {@link android.media.MediaMetadataRetriever#getFrameAtTime(long)} is used to + * extract the video frame. + * + *

When retrieving the frame at the given time position, there is no guarantee that the data + * source has a frame located at the position. When this happens, a frame nearby will be returned. + * If the long is negative, time position and option will ignored, and any frame that the + * implementation considers as representative may be returned. + */ + public static final Option TARGET_FRAME = Option.disk( + "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.TargetFrame", DEFAULT_FRAME, + new Option.CacheKeyUpdater() { + private final ByteBuffer buffer = ByteBuffer.allocate(Long.SIZE / Byte.SIZE); + @Override + public void update(byte[] keyBytes, Long value, MessageDigest messageDigest) { + messageDigest.update(keyBytes); + synchronized (buffer) { + buffer.position(0); + messageDigest.update(buffer.putLong(value).array()); + } + } + }); + + /** + * An integer indicating the frame option used to retrieve a target frame. + * + *

This option will be ignored if {@link #TARGET_FRAME} is not set or is set to + * {@link #DEFAULT_FRAME}. + * + * @see MediaMetadataRetriever#getFrameAtTime(long, int) + */ + // Public API. + @SuppressWarnings("WeakerAccess") + public static final Option FRAME_OPTION = Option.disk( + "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.FrameOption", + null /*defaultValue*/, + new Option.CacheKeyUpdater() { + private final ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE); + @Override + public void update(byte[] keyBytes, Integer value, MessageDigest messageDigest) { + if (value == null) { + return; + } + messageDigest.update(keyBytes); + synchronized (buffer) { + buffer.position(0); + messageDigest.update(buffer.putInt(value).array()); + } + } + } + ); + + private static final MediaMetadataRetrieverFactory DEFAULT_FACTORY = + new MediaMetadataRetrieverFactory(); + + private final MediaMetadataRetrieverInitializer initializer; + private final BitmapPool bitmapPool; + private final MediaMetadataRetrieverFactory factory; + + public static ResourceDecoder asset(BitmapPool bitmapPool) { + return new VideoDecoder<>(bitmapPool, new AssetFileDescriptorInitializer()); + } + + public static ResourceDecoder parcel(BitmapPool bitmapPool) { + return new VideoDecoder<>(bitmapPool, new ParcelFileDescriptorInitializer()); + } + + VideoDecoder( + BitmapPool bitmapPool, MediaMetadataRetrieverInitializer initializer) { + this(bitmapPool, initializer, DEFAULT_FACTORY); + } + + @VisibleForTesting + VideoDecoder( + BitmapPool bitmapPool, + MediaMetadataRetrieverInitializer initializer, + MediaMetadataRetrieverFactory factory) { + this.bitmapPool = bitmapPool; + this.initializer = initializer; + this.factory = factory; + } + + @Override + public boolean handles(T data, Options options) { + // Calling setDataSource is expensive so avoid doing so unless we're actually called. + // For non-videos this isn't any cheaper, but for videos it safes the redundant call and + // 50-100ms. + return true; + } + + @Override + public Resource decode(T resource, int outWidth, int outHeight, + Options options) throws IOException { + long frameTimeMicros = options.get(TARGET_FRAME); + if (frameTimeMicros < 0 && frameTimeMicros != DEFAULT_FRAME) { + throw new IllegalArgumentException( + "Requested frame must be non-negative, or DEFAULT_FRAME, given: " + frameTimeMicros); + } + Integer frameOption = options.get(FRAME_OPTION); + + final Bitmap result; + MediaMetadataRetriever mediaMetadataRetriever = factory.build(); + try { + initializer.initialize(mediaMetadataRetriever, resource); + if (frameTimeMicros == DEFAULT_FRAME) { + result = mediaMetadataRetriever.getFrameAtTime(); + } else if (frameOption == null) { + result = mediaMetadataRetriever.getFrameAtTime(frameTimeMicros); + } else { + result = mediaMetadataRetriever.getFrameAtTime(frameTimeMicros, frameOption); + } + } catch (RuntimeException e) { + // MediaMetadataRetriever APIs throw generic runtime exceptions when given invalid data. + throw new IOException(e); + } finally { + mediaMetadataRetriever.release(); + } + return BitmapResource.obtain(result, bitmapPool); + } + + @VisibleForTesting + static class MediaMetadataRetrieverFactory { + public MediaMetadataRetriever build() { + return new MediaMetadataRetriever(); + } + } + + @VisibleForTesting + interface MediaMetadataRetrieverInitializer { + void initialize(MediaMetadataRetriever retriever, T data); + } + + private static final class AssetFileDescriptorInitializer + implements MediaMetadataRetrieverInitializer { + + @Override + public void initialize(MediaMetadataRetriever retriever, AssetFileDescriptor data) { + retriever.setDataSource(data.getFileDescriptor(), data.getStartOffset(), data.getLength()); + } + } + + // Visible for VideoBitmapDecoder. + static final class ParcelFileDescriptorInitializer + implements MediaMetadataRetrieverInitializer { + + @Override + public void initialize(MediaMetadataRetriever retriever, ParcelFileDescriptor data) { + retriever.setDataSource(data.getFileDescriptor()); + } + } +} diff --git a/library/src/main/java/com/bumptech/glide/request/RequestOptions.java b/library/src/main/java/com/bumptech/glide/request/RequestOptions.java index a45a265ddd..9bf9b5205a 100644 --- a/library/src/main/java/com/bumptech/glide/request/RequestOptions.java +++ b/library/src/main/java/com/bumptech/glide/request/RequestOptions.java @@ -27,7 +27,7 @@ import com.bumptech.glide.load.resource.bitmap.Downsampler; import com.bumptech.glide.load.resource.bitmap.DrawableTransformation; import com.bumptech.glide.load.resource.bitmap.FitCenter; -import com.bumptech.glide.load.resource.bitmap.VideoBitmapDecoder; +import com.bumptech.glide.load.resource.bitmap.VideoDecoder; import com.bumptech.glide.load.resource.gif.GifDrawable; import com.bumptech.glide.load.resource.gif.GifDrawableTransformation; import com.bumptech.glide.load.resource.gif.GifOptions; @@ -888,17 +888,17 @@ public RequestOptions encodeQuality(@IntRange(from = 0, to = 100) int quality) { /** * Sets the time position of the frame to extract from a video. * - *

This is a component option specific to {@link VideoBitmapDecoder}. If the default video + *

This is a component option specific to {@link VideoDecoder}. If the default video * decoder is replaced or skipped because of your configuration, this option may be ignored. * - * @see VideoBitmapDecoder#TARGET_FRAME + * @see VideoDecoder#TARGET_FRAME * @param frameTimeMicros The time position in microseconds of the desired frame. If negative, the * Android framework implementation return a representative frame. */ @NonNull @CheckResult public RequestOptions frame(@IntRange(from = 0) long frameTimeMicros) { - return set(VideoBitmapDecoder.TARGET_FRAME, frameTimeMicros); + return set(VideoDecoder.TARGET_FRAME, frameTimeMicros); } /** diff --git a/library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoBitmapDecoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoDecoderTest.java similarity index 73% rename from library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoBitmapDecoderTest.java rename to library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoDecoderTest.java index 377c85da2d..c7bcc8fad5 100644 --- a/library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoBitmapDecoderTest.java +++ b/library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoDecoderTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -14,7 +13,6 @@ import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.util.Preconditions; -import java.io.FileDescriptor; import java.io.IOException; import org.junit.Before; import org.junit.Test; @@ -26,19 +24,20 @@ @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE, sdk = 18) -public class VideoBitmapDecoderTest { +public class VideoDecoderTest { @Mock private ParcelFileDescriptor resource; - @Mock private VideoBitmapDecoder.MediaMetadataRetrieverFactory factory; + @Mock private VideoDecoder.MediaMetadataRetrieverFactory factory; + @Mock private VideoDecoder.MediaMetadataRetrieverInitializer initializer; @Mock private MediaMetadataRetriever retriever; @Mock private BitmapPool bitmapPool; - private VideoBitmapDecoder decoder; + private VideoDecoder decoder; private Options options; @Before public void setup() { MockitoAnnotations.initMocks(this); when(factory.build()).thenReturn(retriever); - decoder = new VideoBitmapDecoder(bitmapPool, factory); + decoder = new VideoDecoder<>(bitmapPool, initializer, factory); options = new Options(); } @@ -47,12 +46,10 @@ public void testReturnsRetrievedFrameForResource() throws IOException { Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); when(retriever.getFrameAtTime()).thenReturn(expected); - FileDescriptor toSet = FileDescriptor.in; - when(resource.getFileDescriptor()).thenReturn(toSet); Resource result = Preconditions.checkNotNull(decoder.decode(resource, 100, 100, options)); - verify(retriever).setDataSource(eq(toSet)); + verify(initializer).initialize(retriever, resource); assertEquals(expected, result.get()); } @@ -63,24 +60,17 @@ public void testReleasesMediaMetadataRetriever() throws IOException { verify(retriever).release(); } - @Test - public void testClosesResource() throws IOException { - decoder.decode(resource, 1, 2, options); - - verify(resource).close(); - } - @Test(expected = IllegalArgumentException.class) public void testThrowsExceptionIfCalledWithInvalidFrame() throws IOException { - options.set(VideoBitmapDecoder.TARGET_FRAME, -5L); - new VideoBitmapDecoder(bitmapPool, factory).decode(resource, 100, 100, options); + options.set(VideoDecoder.TARGET_FRAME, -5L); + new VideoDecoder<>(bitmapPool, initializer, factory).decode(resource, 100, 100, options); } @Test public void testSpecifiesThumbnailFrameIfICalledWithFrameNumber() throws IOException { long frame = 5; - options.set(VideoBitmapDecoder.TARGET_FRAME, frame); - decoder = new VideoBitmapDecoder(bitmapPool, factory); + options.set(VideoDecoder.TARGET_FRAME, frame); + decoder = new VideoDecoder<>(bitmapPool, initializer, factory); decoder.decode(resource, 100, 100, options); @@ -90,7 +80,7 @@ public void testSpecifiesThumbnailFrameIfICalledWithFrameNumber() throws IOExcep @Test public void testDoesNotSpecifyThumbnailFrameIfCalledWithoutFrameNumber() throws IOException { - decoder = new VideoBitmapDecoder(bitmapPool, factory); + decoder = new VideoDecoder<>(bitmapPool, initializer, factory); decoder.decode(resource, 100, 100, options); verify(retriever).getFrameAtTime();