package com.bumptech.glide.load.resource.gif; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isA; import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import com.bumptech.glide.gifdecoder.GifDecoder; import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; import com.bumptech.glide.load.resource.gif.GifDrawableTest.BitmapTrackingShadowCanvas; import com.bumptech.glide.tests.GlideShadowLooper; import com.bumptech.glide.tests.Util; import java.util.HashSet; import java.util.Set; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowCanvas; @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE, sdk = 18, shadows = { GlideShadowLooper.class, BitmapTrackingShadowCanvas.class }) public class GifDrawableTest { private GifDrawable drawable; private int frameHeight; private int frameWidth; private Bitmap firstFrame; private int initialSdkVersion; @Mock private Drawable.Callback cb; @Mock private BitmapPool bitmapPool; @Mock private GifFrameLoader frameLoader; @Mock private Paint paint; @Mock private Transformation<Bitmap> transformation; private static Paint isAPaint() { return isA(Paint.class); } private static Rect isARect() { return isA(Rect.class); } @Before public void setUp() { MockitoAnnotations.initMocks(this); frameWidth = 120; frameHeight = 450; firstFrame = Bitmap.createBitmap(frameWidth, frameHeight, Bitmap.Config.RGB_565); drawable = new GifDrawable(frameLoader, bitmapPool, paint); when(frameLoader.getWidth()).thenReturn(frameWidth); when(frameLoader.getHeight()).thenReturn(frameHeight); when(frameLoader.getCurrentFrame()).thenReturn(firstFrame); when(frameLoader.getCurrentIndex()).thenReturn(0); drawable.setCallback(cb); initialSdkVersion = Build.VERSION.SDK_INT; } @After public void tearDown() { Util.setSdkVersionInt(initialSdkVersion); } @Test public void testShouldDrawFirstFrameBeforeAnyFrameRead() { Canvas canvas = new Canvas(); drawable.draw(canvas); BitmapTrackingShadowCanvas shadowCanvas = (BitmapTrackingShadowCanvas) Shadow.extract(canvas); assertThat(shadowCanvas.getDrawnBitmaps()).containsExactly(firstFrame); } @Test public void testDoesDrawCurrentFrameIfOneIsAvailable() { Canvas canvas = mock(Canvas.class); Bitmap currentFrame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_4444); when(frameLoader.getCurrentFrame()).thenReturn(currentFrame); drawable.draw(canvas); verify(canvas).drawBitmap(eq(currentFrame), (Rect) isNull(), isARect(), isAPaint()); verify(canvas, never()).drawBitmap(eq(firstFrame), (Rect) isNull(), isARect(), isAPaint()); } @Test public void testRequestsNextFrameOnStart() { drawable.setVisible(true, true); drawable.start(); verify(frameLoader).subscribe(eq(drawable)); } @Test public void testRequestsNextFrameOnStartWithoutCallToSetVisible() { drawable.start(); verify(frameLoader).subscribe(eq(drawable)); } @Test public void testDoesNotRequestNextFrameOnStartIfGotCallToSetVisibleWithVisibleFalse() { drawable.setVisible(false, false); drawable.start(); verify(frameLoader, never()).subscribe(eq(drawable)); } @Test public void testDoesNotRequestNextFrameOnStartIfHasSingleFrame() { when(frameLoader.getFrameCount()).thenReturn(1); drawable.setVisible(true, false); drawable.start(); verify(frameLoader, never()).subscribe(eq(drawable)); } @Test public void testInvalidatesSelfOnStartIfHasSingleFrame() { when(frameLoader.getFrameCount()).thenReturn(1); drawable.setVisible(true, false); drawable.start(); verify(cb).invalidateDrawable(eq(drawable)); } @Test public void testShouldInvalidateSelfOnRun() { drawable.setVisible(true, true); drawable.start(); verify(cb).invalidateDrawable(eq(drawable)); } @Test public void testShouldNotScheduleItselfIfAlreadyRunning() { drawable.setVisible(true, true); drawable.start(); drawable.start(); verify(frameLoader, times(1)).subscribe(eq(drawable)); } @Test public void testReturnsFalseFromIsRunningWhenNotRunning() { assertFalse(drawable.isRunning()); } @Test public void testReturnsTrueFromIsRunningWhenRunning() { drawable.setVisible(true, true); drawable.start(); assertTrue(drawable.isRunning()); } @Test public void testInvalidatesSelfWhenFrameReady() { drawable.setIsRunning(true); drawable.onFrameReady(); verify(cb).invalidateDrawable(eq(drawable)); } @Test public void testDoesNotStartLoadingNextFrameWhenCurrentFinishesIfHasNoCallback() { drawable.setIsRunning(true); drawable.setCallback(null); drawable.onFrameReady(); verify(frameLoader).unsubscribe(eq(drawable)); } @Test public void testStopsWhenCurrentFrameFinishesIfHasNoCallback() { drawable.setIsRunning(true); drawable.setCallback(null); drawable.onFrameReady(); assertFalse(drawable.isRunning()); } @Test public void testUnsubscribesWhenCurrentFinishesIfHasNoCallback() { drawable.setIsRunning(true); drawable.setCallback(null); drawable.onFrameReady(); verify(frameLoader).unsubscribe(eq(drawable)); } @Test public void testSetsIsRunningFalseOnStop() { drawable.start(); drawable.stop(); assertFalse(drawable.isRunning()); } @Test public void testStopsOnSetVisibleFalse() { drawable.start(); drawable.setVisible(false, true); assertFalse(drawable.isRunning()); } @Test public void testStartsOnSetVisibleTrueIfRunning() { drawable.start(); drawable.setVisible(false, false); drawable.setVisible(true, true); assertTrue(drawable.isRunning()); } @Test public void testDoesNotStartOnVisibleTrueIfNotRunning() { drawable.setVisible(true, true); assertFalse(drawable.isRunning()); } @Test public void testDoesNotStartOnSetVisibleIfStartedAndStopped() { drawable.start(); drawable.stop(); drawable.setVisible(true, true); assertFalse(drawable.isRunning()); } @Test public void testDoesNotImmediatelyRunIfStartedWhileNotVisible() { drawable.setVisible(false, false); drawable.start(); assertFalse(drawable.isRunning()); } @Test public void testGetOpacityReturnsTransparent() { assertEquals(PixelFormat.TRANSPARENT, drawable.getOpacity()); } @Test public void testReturnsFrameCountFromDecoder() { int expected = 4; when(frameLoader.getFrameCount()).thenReturn(expected); assertEquals(expected, drawable.getFrameCount()); } @Test public void testReturnsDefaultFrameIndex() { final int expected = -1; when(frameLoader.getCurrentIndex()).thenReturn(expected); assertEquals(expected, drawable.getFrameIndex()); } @Test public void testReturnsNonDefaultFrameIndex() { final int expected = 100; when(frameLoader.getCurrentIndex()).thenReturn(expected); assertEquals(expected, drawable.getFrameIndex()); } @Test public void testRecycleCallsClearOnFrameManager() { drawable.recycle(); verify(frameLoader).clear(); } @Test public void testIsNotRecycledIfNotRecycled() { assertFalse(drawable.isRecycled()); } @Test public void testIsRecycledAfterRecycled() { drawable.recycle(); assertTrue(drawable.isRecycled()); } @Test public void testReturnsNonNullConstantState() { assertNotNull(drawable.getConstantState()); } @Test public void testReturnsSizeFromFrameLoader() { int size = 1243; when(frameLoader.getSize()).thenReturn(size); assertThat(drawable.getSize()).isEqualTo(size); } @Test public void testReturnsNewDrawableFromConstantState() { Bitmap firstFrame = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); drawable = new GifDrawable(RuntimeEnvironment.application, mock(GifDecoder.class), bitmapPool, transformation, 100, 100, firstFrame); assertNotNull(drawable.getConstantState().newDrawable()); assertNotNull( drawable.getConstantState().newDrawable(RuntimeEnvironment.application.getResources())); } @Test public void testReturnsFrameWidthAndHeightForIntrinsicDimensions() { assertEquals(frameWidth, drawable.getIntrinsicWidth()); assertEquals(frameHeight, drawable.getIntrinsicHeight()); } @Test public void testLoopsASingleTimeIfLoopCountIsSetToOne() { final int loopCount = 1; final int frameCount = 2; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(loopCount); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testLoopsForeverIfLoopCountIsSetToLoopForever() { final int loopCount = 40; final int frameCount = 2; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(GifDrawable.LOOP_FOREVER); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertTrue("drawable should be still running", drawable.isRunning()); } @Test public void testLoopsOnceIfLoopCountIsSetToOneWithThreeFrames() { final int loopCount = 1; final int frameCount = 3; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(loopCount); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testLoopsThreeTimesIfLoopCountIsSetToThree() { final int loopCount = 3; final int frameCount = 2; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(loopCount); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testCallingStartResetsLoopCounter() { when(frameLoader.getFrameCount()).thenReturn(2); drawable.setLoopCount(1); drawable.setVisible(true, true); drawable.start(); drawable.onFrameReady(); when(frameLoader.getCurrentIndex()).thenReturn(1); drawable.onFrameReady(); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); drawable.start(); when(frameLoader.getCurrentIndex()).thenReturn(0); drawable.onFrameReady(); when(frameLoader.getCurrentIndex()).thenReturn(1); drawable.onFrameReady(); // 4 onFrameReady(), 2 start() verify(cb, times(4 + 2)).invalidateDrawable(eq(drawable)); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testChangingTheLoopCountAfterHittingTheMaxLoopCount() { final int initialLoopCount = 1; final int frameCount = 2; when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(initialLoopCount); drawable.setVisible(true, true); drawable.start(); runLoops(initialLoopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); final int newLoopCount = 2; drawable.setLoopCount(newLoopCount); drawable.start(); runLoops(newLoopCount, frameCount); int numStarts = 2; int expectedFrames = (initialLoopCount + newLoopCount) * frameCount + numStarts; verify(cb, times(expectedFrames)).invalidateDrawable(eq(drawable)); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test(expected = IllegalArgumentException.class) public void testThrowsIfGivenLoopCountLessThanZeroAndNotInfinite() { drawable.setLoopCount(-2); } @Test public void testUsesDecoderTotalLoopCountIfLoopCountIsLoopIntrinsic() { final int frameCount = 3; final int loopCount = 2; when(frameLoader.getLoopCount()).thenReturn(loopCount); when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(GifDrawable.LOOP_INTRINSIC); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertFalse("drawable should be stopped after loop is completed", drawable.isRunning()); } @Test public void testLoopsForeverIfLoopCountIsLoopIntrinsicAndTotalIterationCountIsForever() { final int frameCount = 3; final int loopCount = 40; when(frameLoader.getLoopCount()).thenReturn(GifDecoder.TOTAL_ITERATION_COUNT_FOREVER); when(frameLoader.getFrameCount()).thenReturn(frameCount); drawable.setLoopCount(GifDrawable.LOOP_INTRINSIC); drawable.setVisible(true, true); drawable.start(); runLoops(loopCount, frameCount); verifyRanLoops(loopCount, frameCount); assertTrue("drawable should be still running", drawable.isRunning()); } @Test public void testDoesNotDrawFrameAfterRecycle() { Bitmap bitmap = Bitmap.createBitmap(100, 112341, Bitmap.Config.RGB_565); drawable.setVisible(true, true); drawable.start(); when(frameLoader.getCurrentFrame()).thenReturn(bitmap); drawable.onFrameReady(); drawable.recycle(); Canvas canvas = mock(Canvas.class); drawable.draw(canvas); verify(canvas, never()).drawBitmap(eq(bitmap), isARect(), isARect(), isAPaint()); } @Test public void testSetsFrameTransformationOnFrameManager() { Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); drawable.setFrameTransformation(transformation, bitmap); verify(frameLoader).setFrameTransformation(eq(transformation), eq(bitmap)); } @Test(expected = NullPointerException.class) public void testThrowsIfConstructedWithNullFirstFrame() { new GifDrawable(RuntimeEnvironment.application, mock(GifDecoder.class), bitmapPool, transformation, 100, 100, null); } @Test public void testAppliesGravityOnDrawAfterBoundsChange() { Rect bounds = new Rect(0, 0, frameWidth * 2, frameHeight * 2); drawable.setBounds(bounds); Canvas canvas = mock(Canvas.class); drawable.draw(canvas); verify(canvas).drawBitmap(isA(Bitmap.class), (Rect) isNull(), eq(bounds), eq(paint)); } @Test public void testSetAlphaSetsAlphaOnPaint() { int alpha = 100; drawable.setAlpha(alpha); verify(paint).setAlpha(eq(alpha)); } @Test public void testSetColorFilterSetsColorFilterOnPaint() { ColorFilter colorFilter = new ColorFilter(); drawable.setColorFilter(colorFilter); verify(paint).setColorFilter(eq(colorFilter)); } @Test public void testReturnsCurrentTransformationInGetFrameTransformation() { @SuppressWarnings("unchecked") Transformation<Bitmap> newTransformation = mock(Transformation.class); Bitmap bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); drawable.setFrameTransformation(newTransformation, bitmap); verify(frameLoader).setFrameTransformation(eq(newTransformation), eq(bitmap)); } @Test(expected = NullPointerException.class) public void testThrowsIfCreatedWithNullState() { new GifDrawable(null); } private void verifyRanLoops(int loopCount, int frameCount) { // 1 for invalidate in start(). verify(cb, times(1 + loopCount * frameCount)).invalidateDrawable(eq(drawable)); } private void runLoops(int loopCount, int frameCount) { for (int loop = 0; loop < loopCount; loop++) { for (int frame = 0; frame < frameCount; frame++) { when(frameLoader.getCurrentIndex()).thenReturn(frame); assertTrue("drawable should be started before calling drawable.onFrameReady()", drawable.isRunning()); drawable.onFrameReady(); } } } /** * Keeps track of the set of Bitmaps drawn to the canvas. */ @Implements(Canvas.class) public static class BitmapTrackingShadowCanvas extends ShadowCanvas { private final Set<Bitmap> drawnBitmaps = new HashSet<>(); @Implementation public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { drawnBitmaps.add(bitmap); } public Iterable<Bitmap> getDrawnBitmaps() { return drawnBitmaps; } } }