diff --git a/gradle.properties b/gradle.properties index 1545f61aa5..65e27a5e36 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,7 +25,7 @@ JUNIT_VERSION=4.13-SNAPSHOT # Matches the version in Google. MOCKITO_VERSION=1.9.5 MOCKITO_ANDROID_VERSION=2.11.0 -ROBOLECTRIC_VERSION=3.3.2 +ROBOLECTRIC_VERSION=3.6.1 MOCKWEBSERVER_VERSION=3.0.0-RC1 TRUTH_VERSION=0.36 JSR_305_VERSION=3.0.2 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 index f1b7c32d59..c58e781dc5 100644 --- 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 @@ -1,9 +1,14 @@ package com.bumptech.glide.load.resource.bitmap; +import android.annotation.TargetApi; import android.content.res.AssetFileDescriptor; import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; +import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.ParcelFileDescriptor; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import com.bumptech.glide.load.Option; import com.bumptech.glide.load.Options; @@ -30,6 +35,10 @@ public class VideoDecoder implements ResourceDecoder { */ public static final long DEFAULT_FRAME = -1; + /** Matches the behavior of {@link MediaMetadataRetriever#getFrameAtTime(long)}. */ + @VisibleForTesting + static final int DEFAULT_FRAME_OPTION = MediaMetadataRetriever.OPTION_CLOSEST_SYNC; + /** * 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 @@ -66,7 +75,7 @@ public void update(byte[] keyBytes, Long value, MessageDigest messageDigest) { @SuppressWarnings("WeakerAccess") public static final Option FRAME_OPTION = Option.disk( "com.bumptech.glide.load.resource.bitmap.VideoBitmapDecode.FrameOption", - null /*defaultValue*/, + /*defaultValue=*/ MediaMetadataRetriever.OPTION_CLOSEST_SYNC, new Option.CacheKeyUpdater() { private final ByteBuffer buffer = ByteBuffer.allocate(Integer.SIZE / Byte.SIZE); @Override @@ -114,7 +123,7 @@ public static ResourceDecoder parcel(BitmapPool bi } @Override - public boolean handles(T data, Options options) { + public boolean handles(@NonNull T 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 safes the redundant call and // 50-100ms. @@ -122,35 +131,51 @@ public boolean handles(T data, Options options) { } @Override - public Resource decode(T resource, int outWidth, int outHeight, - Options options) throws IOException { + public Resource decode( + @NonNull T 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); + if (frameOption == null) { + frameOption = DEFAULT_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); - } + result = + decodeFrame(mediaMetadataRetriever, frameTimeMicros, frameOption, outWidth, outHeight); } 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); } + @TargetApi(Build.VERSION_CODES.O_MR1) + @Nullable + private static Bitmap decodeFrame( + MediaMetadataRetriever mediaMetadataRetriever, + long frameTimeMicros, + int frameOption, + int outWidth, + int outHeight) { + if (Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1) { + return mediaMetadataRetriever.getScaledFrameAtTime( + frameTimeMicros, frameOption, outWidth, outHeight); + } else { + return mediaMetadataRetriever.getFrameAtTime(frameTimeMicros, frameOption); + } + } + @VisibleForTesting static class MediaMetadataRetrieverFactory { public MediaMetadataRetriever build() { diff --git a/library/src/test/java/com/bumptech/glide/load/engine/cache/MemorySizeCalculatorTest.java b/library/src/test/java/com/bumptech/glide/load/engine/cache/MemorySizeCalculatorTest.java index 396aa31c18..ae2eb4bd63 100644 --- a/library/src/test/java/com/bumptech/glide/load/engine/cache/MemorySizeCalculatorTest.java +++ b/library/src/test/java/com/bumptech/glide/load/engine/cache/MemorySizeCalculatorTest.java @@ -195,6 +195,7 @@ void setIsLowRam() { } @Implementation + @Override public boolean isLowRamDevice() { return isLowRam; } diff --git a/library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoDecoderTest.java b/library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoDecoderTest.java index c7bcc8fad5..e886c51fab 100644 --- a/library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoDecoderTest.java +++ b/library/src/test/java/com/bumptech/glide/load/resource/bitmap/VideoDecoderTest.java @@ -1,19 +1,21 @@ package com.bumptech.glide.load.resource.bitmap; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; +import android.os.Build; import android.os.ParcelFileDescriptor; import com.bumptech.glide.load.Options; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.tests.Util; import com.bumptech.glide.util.Preconditions; import java.io.IOException; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,7 +25,7 @@ import org.robolectric.annotation.Config; @RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE, sdk = 18) +@Config(manifest = Config.NONE, sdk = 27) public class VideoDecoderTest { @Mock private ParcelFileDescriptor resource; @Mock private VideoDecoder.MediaMetadataRetrieverFactory factory; @@ -32,6 +34,7 @@ public class VideoDecoderTest { @Mock private BitmapPool bitmapPool; private VideoDecoder decoder; private Options options; + private int initialSdkVersion; @Before public void setup() { @@ -39,12 +42,21 @@ public void setup() { when(factory.build()).thenReturn(retriever); decoder = new VideoDecoder<>(bitmapPool, initializer, factory); options = new Options(); + + initialSdkVersion = Build.VERSION.SDK_INT; + } + + @After + public void tearDown() { + Util.setSdkVersionInt(initialSdkVersion); } @Test public void testReturnsRetrievedFrameForResource() throws IOException { + Util.setSdkVersionInt(19); Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); - when(retriever.getFrameAtTime()).thenReturn(expected); + when(retriever.getFrameAtTime(VideoDecoder.DEFAULT_FRAME, VideoDecoder.DEFAULT_FRAME_OPTION)) + .thenReturn(expected); Resource result = Preconditions.checkNotNull(decoder.decode(resource, 100, 100, options)); @@ -55,6 +67,7 @@ public void testReturnsRetrievedFrameForResource() throws IOException { @Test public void testReleasesMediaMetadataRetriever() throws IOException { + Util.setSdkVersionInt(19); decoder.decode(resource, 1, 2, options); verify(retriever).release(); @@ -62,28 +75,38 @@ public void testReleasesMediaMetadataRetriever() throws IOException { @Test(expected = IllegalArgumentException.class) public void testThrowsExceptionIfCalledWithInvalidFrame() throws IOException { + Util.setSdkVersionInt(19); options.set(VideoDecoder.TARGET_FRAME, -5L); new VideoDecoder<>(bitmapPool, initializer, factory).decode(resource, 100, 100, options); } @Test public void testSpecifiesThumbnailFrameIfICalledWithFrameNumber() throws IOException { + Util.setSdkVersionInt(19); long frame = 5; options.set(VideoDecoder.TARGET_FRAME, frame); decoder = new VideoDecoder<>(bitmapPool, initializer, factory); decoder.decode(resource, 100, 100, options); - verify(retriever).getFrameAtTime(frame); - verify(retriever, never()).getFrameAtTime(); + verify(retriever).getFrameAtTime(frame, VideoDecoder.DEFAULT_FRAME_OPTION); } @Test public void testDoesNotSpecifyThumbnailFrameIfCalledWithoutFrameNumber() throws IOException { + Util.setSdkVersionInt(19); decoder = new VideoDecoder<>(bitmapPool, initializer, factory); decoder.decode(resource, 100, 100, options); - verify(retriever).getFrameAtTime(); - verify(retriever, never()).getFrameAtTime(anyLong()); + verify(retriever).getFrameAtTime(VideoDecoder.DEFAULT_FRAME, VideoDecoder.DEFAULT_FRAME_OPTION); + } + + @Test + public void getScaledFrameAtTime() throws IOException { + Bitmap expected = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + when(retriever.getScaledFrameAtTime(-1, MediaMetadataRetriever.OPTION_CLOSEST_SYNC, 100, 100)) + .thenReturn(expected); + + assertThat(decoder.decode(resource, 100, 100, options).get()).isSameAs(expected); } }