/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.fresco.animation.bitmap; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import com.facebook.common.references.CloseableReference; import com.facebook.common.references.ResourceReleaser; import com.facebook.fresco.animation.backend.AnimationBackend; import com.facebook.fresco.animation.backend.AnimationInformation; import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparationStrategy; import com.facebook.fresco.animation.bitmap.preparation.BitmapFramePreparer; import com.facebook.imagepipeline.bitmaps.PlatformBitmapFactory; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import static org.fest.assertions.api.Assertions.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import org.robolectric.RobolectricTestRunner; /** * Tests {@link BitmapAnimationBackend} */ @RunWith(RobolectricTestRunner.class) public class BitmapAnimationBackendTest { @Mock public PlatformBitmapFactory mPlatformBitmapFactory; @Mock public BitmapFrameCache mBitmapFrameCache; @Mock public AnimationInformation mAnimationInformation; @Mock public BitmapFrameRenderer mBitmapFrameRenderer; @Mock public Rect mBounds; @Mock public Drawable mParentDrawable; @Mock public Canvas mCanvas; @Mock public Bitmap mBitmap; @Mock public ResourceReleaser<Bitmap> mBitmapResourceReleaser; @Mock public BitmapAnimationBackend.FrameListener mFrameListener; @Mock public BitmapFramePreparationStrategy mBitmapFramePreparationStrategy; @Mock public BitmapFramePreparer mBitmapFramePreparer; @Captor public ArgumentCaptor<CloseableReference<Bitmap>> mCapturedBitmapReference; private CloseableReference<Bitmap> mBitmapRefererence; private BitmapAnimationBackend mBitmapAnimationBackend; @Before public void setup() { MockitoAnnotations.initMocks(this); mBitmapRefererence = CloseableReference.of(mBitmap, mBitmapResourceReleaser); mBitmapAnimationBackend = new BitmapAnimationBackend( mPlatformBitmapFactory, mBitmapFrameCache, mAnimationInformation, mBitmapFrameRenderer, mBitmapFramePreparationStrategy, mBitmapFramePreparer); mBitmapAnimationBackend.setFrameListener(mFrameListener); } @Test public void testSetBounds() { mBitmapAnimationBackend.setBounds(mBounds); verify(mBitmapFrameRenderer).setBounds(mBounds); } @Test public void testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionsUnset() { int boundsWidth = 160; int boundsHeight = 90; int backendIntrinsicWidth = AnimationBackend.INTRINSIC_DIMENSION_UNSET; int backendIntrinsicHeight = AnimationBackend.INTRINSIC_DIMENSION_UNSET; setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight); mBitmapAnimationBackend.setBounds(mBounds); assertThat(mBitmapAnimationBackend.getIntrinsicWidth()).isEqualTo(boundsWidth); assertThat(mBitmapAnimationBackend.getIntrinsicHeight()).isEqualTo(boundsHeight); } @Test public void testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionWidthSet() { int boundsWidth = 160; int boundsHeight = 90; int backendIntrinsicWidth = 260; int backendIntrinsicHeight = AnimationBackend.INTRINSIC_DIMENSION_UNSET; setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight); mBitmapAnimationBackend.setBounds(mBounds); assertThat(mBitmapAnimationBackend.getIntrinsicWidth()).isEqualTo(backendIntrinsicWidth); assertThat(mBitmapAnimationBackend.getIntrinsicHeight()).isEqualTo(boundsHeight); } @Test public void testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionHeightSet() { int boundsWidth = 160; int boundsHeight = 90; int backendIntrinsicWidth = AnimationBackend.INTRINSIC_DIMENSION_UNSET; int backendIntrinsicHeight = 260; setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight); mBitmapAnimationBackend.setBounds(mBounds); assertThat(mBitmapAnimationBackend.getIntrinsicWidth()).isEqualTo(boundsWidth); assertThat(mBitmapAnimationBackend.getIntrinsicHeight()).isEqualTo(backendIntrinsicHeight); } @Test public void testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionsSet() { int boundsWidth = 160; int boundsHeight = 90; int backendIntrinsicWidth = 260; int backendIntrinsicHeight = 300; setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight); mBitmapAnimationBackend.setBounds(mBounds); assertThat(mBitmapAnimationBackend.getIntrinsicWidth()).isEqualTo(backendIntrinsicWidth); assertThat(mBitmapAnimationBackend.getIntrinsicHeight()).isEqualTo(backendIntrinsicHeight); } @Test public void testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionsUnsetAndNullBounds() { int boundsWidth = 160; int boundsHeight = 90; int backendIntrinsicWidth = AnimationBackend.INTRINSIC_DIMENSION_UNSET; int backendIntrinsicHeight = AnimationBackend.INTRINSIC_DIMENSION_UNSET; setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight); mBitmapAnimationBackend.setBounds(null); assertThat(mBitmapAnimationBackend.getIntrinsicWidth()) .isEqualTo(AnimationBackend.INTRINSIC_DIMENSION_UNSET); assertThat(mBitmapAnimationBackend.getIntrinsicHeight()) .isEqualTo(AnimationBackend.INTRINSIC_DIMENSION_UNSET); } @Test public void testSetBoundsUpdatesIntrinsicDimensionsWhenBackendDimensionsSetAndNullBounds() { int boundsWidth = 160; int boundsHeight = 90; int backendIntrinsicWidth = 260; int backendIntrinsicHeight = 300; setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight); mBitmapAnimationBackend.setBounds(null); assertThat(mBitmapAnimationBackend.getIntrinsicWidth()) .isEqualTo(backendIntrinsicWidth); assertThat(mBitmapAnimationBackend.getIntrinsicHeight()) .isEqualTo(backendIntrinsicHeight); } @Test public void testSetBoundsUpdatesIntrinsicDimensionsWhenBackendWidthSetAndNullBounds() { int boundsWidth = 160; int boundsHeight = 90; int backendIntrinsicWidth = 260; int backendIntrinsicHeight = AnimationBackend.INTRINSIC_DIMENSION_UNSET; setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight); mBitmapAnimationBackend.setBounds(null); assertThat(mBitmapAnimationBackend.getIntrinsicWidth()) .isEqualTo(backendIntrinsicWidth); assertThat(mBitmapAnimationBackend.getIntrinsicHeight()) .isEqualTo(backendIntrinsicHeight); } @Test public void testSetBoundsUpdatesIntrinsicDimensionsWhenBackendHeightSetAndNullBounds() { int boundsWidth = 160; int boundsHeight = 90; int backendIntrinsicWidth = AnimationBackend.INTRINSIC_DIMENSION_UNSET; int backendIntrinsicHeight = 400; setupBoundsAndRendererDimensions( boundsWidth, boundsHeight, backendIntrinsicWidth, backendIntrinsicHeight); mBitmapAnimationBackend.setBounds(null); assertThat(mBitmapAnimationBackend.getIntrinsicWidth()) .isEqualTo(backendIntrinsicWidth); assertThat(mBitmapAnimationBackend.getIntrinsicHeight()) .isEqualTo(backendIntrinsicHeight); } @Test public void testGetFrameCount() { when(mAnimationInformation.getFrameCount()).thenReturn(123); assertThat(mBitmapAnimationBackend.getFrameCount()).isEqualTo(123); } @Test public void testGetLoopCount() { when(mAnimationInformation.getLoopCount()).thenReturn(AnimationInformation.LOOP_COUNT_INFINITE); assertThat(mBitmapAnimationBackend.getLoopCount()) .isEqualTo(AnimationInformation.LOOP_COUNT_INFINITE); when(mAnimationInformation.getLoopCount()).thenReturn(123); assertThat(mBitmapAnimationBackend.getLoopCount()).isEqualTo(123); } @Test public void testGetFrameDuration() { when(mAnimationInformation.getFrameDurationMs(1)).thenReturn(50); when(mAnimationInformation.getFrameDurationMs(2)).thenReturn(100); assertThat(mBitmapAnimationBackend.getFrameDurationMs(1)) .isEqualTo(50); assertThat(mBitmapAnimationBackend.getFrameDurationMs(2)) .isEqualTo(100); } @Test public void testDrawCachedBitmap() { when(mBitmapFrameCache.getCachedFrame(anyInt())) .thenReturn(mBitmapRefererence); mBitmapAnimationBackend.drawFrame(mParentDrawable, mCanvas, 1); verify(mFrameListener).onDrawFrameStart(mBitmapAnimationBackend, 1); verify(mBitmapFrameCache).getCachedFrame(1); verify(mCanvas).drawBitmap(eq(mBitmap), eq(0f), eq(0f), any(Paint.class)); verifyFramePreparationStrategyCalled(1); verifyListenersAndCacheNotified(1, BitmapAnimationBackend.FRAME_TYPE_CACHED); assertReferencesClosed(); } @Test public void testDrawReusedBitmap() { when(mBitmapFrameCache.getBitmapToReuseForFrame(anyInt(), anyInt(), anyInt())) .thenReturn(mBitmapRefererence); when(mBitmapFrameRenderer.renderFrame(anyInt(), any(Bitmap.class))).thenReturn(true); mBitmapAnimationBackend.drawFrame(mParentDrawable, mCanvas, 1); verify(mFrameListener).onDrawFrameStart(mBitmapAnimationBackend, 1); verify(mBitmapFrameCache).getCachedFrame(1); verify(mBitmapFrameCache).getBitmapToReuseForFrame(1, 0, 0); verify(mBitmapFrameRenderer).renderFrame(1, mBitmap); verify(mCanvas).drawBitmap(eq(mBitmap), eq(0f), eq(0f), any(Paint.class)); verifyFramePreparationStrategyCalled(1); verifyListenersAndCacheNotified(1, BitmapAnimationBackend.FRAME_TYPE_REUSED); assertReferencesClosed(); } @Test public void testDrawNewBitmap() { when(mPlatformBitmapFactory.createBitmap(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenReturn(mBitmapRefererence); when(mBitmapFrameRenderer.renderFrame(anyInt(), any(Bitmap.class))).thenReturn(true); mBitmapAnimationBackend.drawFrame(mParentDrawable, mCanvas, 2); verify(mFrameListener).onDrawFrameStart(mBitmapAnimationBackend, 2); verify(mBitmapFrameCache).getCachedFrame(2); verify(mBitmapFrameCache).getBitmapToReuseForFrame(2, 0, 0); verify(mPlatformBitmapFactory).createBitmap(0, 0, Bitmap.Config.ARGB_8888); verify(mBitmapFrameRenderer).renderFrame(2, mBitmap); verify(mCanvas).drawBitmap(eq(mBitmap), eq(0f), eq(0f), any(Paint.class)); verifyFramePreparationStrategyCalled(2); verifyListenersAndCacheNotified(2, BitmapAnimationBackend.FRAME_TYPE_CREATED); assertReferencesClosed(); } @Test public void testDrawNewBitmapWithBounds() { int width = 160; int height = 90; when(mBounds.width()).thenReturn(width); when(mBounds.height()).thenReturn(height); when(mPlatformBitmapFactory.createBitmap(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenReturn(mBitmapRefererence); when(mBitmapFrameRenderer.renderFrame(anyInt(), any(Bitmap.class))).thenReturn(true); when(mBitmapFrameRenderer.getIntrinsicWidth()) .thenReturn(AnimationBackend.INTRINSIC_DIMENSION_UNSET); when(mBitmapFrameRenderer.getIntrinsicHeight()) .thenReturn(AnimationBackend.INTRINSIC_DIMENSION_UNSET); mBitmapAnimationBackend.setBounds(mBounds); mBitmapAnimationBackend.drawFrame(mParentDrawable, mCanvas, 2); verify(mFrameListener).onDrawFrameStart(mBitmapAnimationBackend, 2); verify(mBitmapFrameCache).getCachedFrame(2); verify(mBitmapFrameCache).getBitmapToReuseForFrame(2, width, height); verify(mPlatformBitmapFactory).createBitmap(width, height, Bitmap.Config.ARGB_8888); verify(mBitmapFrameRenderer).renderFrame(2, mBitmap); verify(mCanvas).drawBitmap(eq(mBitmap), isNull(Rect.class), eq(mBounds), any(Paint.class)); verifyFramePreparationStrategyCalled(2); verifyListenersAndCacheNotified(2, BitmapAnimationBackend.FRAME_TYPE_CREATED); assertReferencesClosed(); } @Test public void testDrawFallbackBitmapWhenCreateBitmapNotWorking() { when(mBitmapFrameCache.getFallbackFrame(anyInt())).thenReturn(mBitmapRefererence); when(mBitmapFrameRenderer.renderFrame(anyInt(), any(Bitmap.class))).thenReturn(true); mBitmapAnimationBackend.drawFrame(mParentDrawable, mCanvas, 3); verify(mFrameListener).onDrawFrameStart(mBitmapAnimationBackend, 3); verify(mBitmapFrameCache).getCachedFrame(3); verify(mBitmapFrameCache).getBitmapToReuseForFrame(3, 0, 0); verify(mPlatformBitmapFactory).createBitmap(0, 0, Bitmap.Config.ARGB_8888); verify(mBitmapFrameCache).getFallbackFrame(3); verify(mCanvas).drawBitmap(eq(mBitmap), eq(0f), eq(0f), any(Paint.class)); verifyFramePreparationStrategyCalled(3); verifyListenersNotifiedWithoutCache(3, BitmapAnimationBackend.FRAME_TYPE_FALLBACK); assertReferencesClosed(); } @Test public void testDrawFallbackBitmapWhenRenderFrameNotWorking() { when(mBitmapFrameCache.getFallbackFrame(anyInt())).thenReturn(mBitmapRefererence); // Return a different bitmap for PlatformBitmapFactory CloseableReference<Bitmap> temporaryBitmap = CloseableReference.of(mBitmap, mBitmapResourceReleaser); when(mPlatformBitmapFactory.createBitmap(anyInt(), anyInt(), any(Bitmap.Config.class))) .thenReturn(temporaryBitmap); when(mBitmapFrameRenderer.renderFrame(anyInt(), any(Bitmap.class))).thenReturn(false); mBitmapAnimationBackend.drawFrame(mParentDrawable, mCanvas, 3); verify(mFrameListener).onDrawFrameStart(mBitmapAnimationBackend, 3); verify(mBitmapFrameCache).getCachedFrame(3); verify(mBitmapFrameCache).getBitmapToReuseForFrame(3, 0, 0); verify(mPlatformBitmapFactory).createBitmap(0, 0, Bitmap.Config.ARGB_8888); // Verify that the bitmap has been closed assertThat(temporaryBitmap.isValid()).isFalse(); verify(mBitmapFrameCache).getFallbackFrame(3); verify(mCanvas).drawBitmap(eq(mBitmap), eq(0f), eq(0f), any(Paint.class)); verifyFramePreparationStrategyCalled(3); verifyListenersNotifiedWithoutCache(3, BitmapAnimationBackend.FRAME_TYPE_FALLBACK); assertReferencesClosed(); } @Test public void testDrawNoFrame() { mBitmapAnimationBackend.drawFrame(mParentDrawable, mCanvas, 4); verify(mFrameListener).onDrawFrameStart(mBitmapAnimationBackend, 4); verify(mBitmapFrameCache).getCachedFrame(4); verify(mBitmapFrameCache).getBitmapToReuseForFrame(4, 0, 0); verify(mPlatformBitmapFactory).createBitmap(0, 0, Bitmap.Config.ARGB_8888); verify(mBitmapFrameCache).getFallbackFrame(4); verifyNoMoreInteractions(mCanvas, mBitmapFrameCache); verifyFramePreparationStrategyCalled(4); verify(mFrameListener) .onFrameDropped(mBitmapAnimationBackend, 4); } private void verifyFramePreparationStrategyCalled(int frameNumber) { verify(mBitmapFramePreparationStrategy).prepareFrames( mBitmapFramePreparer, mBitmapFrameCache, mBitmapAnimationBackend, frameNumber); } private void verifyListenersAndCacheNotified( int frameNumber, @BitmapAnimationBackend.FrameType int frameType) { // Verify cache callback verify(mBitmapFrameCache).onFrameRendered( eq(frameNumber), mCapturedBitmapReference.capture(), eq(frameType)); assertThat(mCapturedBitmapReference.getValue()).isEqualTo(mBitmapRefererence); // Verify frame listener verify(mFrameListener).onFrameDrawn(mBitmapAnimationBackend, frameNumber, frameType); } private void verifyListenersNotifiedWithoutCache( int frameNumber, @BitmapAnimationBackend.FrameType int frameType) { // Verify cache callback verify(mBitmapFrameCache, never()).onFrameRendered( anyInt(), mCapturedBitmapReference.capture(), eq(frameType)); // Verify frame listener verify(mFrameListener).onFrameDrawn(mBitmapAnimationBackend, frameNumber, frameType); } private void assertReferencesClosed() { assertThat(mBitmapRefererence.isValid()).isFalse(); } private void setupBoundsAndRendererDimensions( int boundsWidth, int boundsHeight, int backendIntrinsicWidth, int backendIntrinsicHeight) { when(mBounds.width()).thenReturn(boundsWidth); when(mBounds.height()).thenReturn(boundsHeight); when(mBitmapFrameRenderer.getIntrinsicWidth()) .thenReturn(backendIntrinsicWidth); when(mBitmapFrameRenderer.getIntrinsicHeight()) .thenReturn(backendIntrinsicHeight); } }