/* * 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.imagepipeline.animated.base; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import com.facebook.common.references.CloseableReference; import com.facebook.common.references.ResourceReleaser; import com.facebook.imagepipeline.testing.FakeClock; import org.robolectric.RobolectricTestRunner; import com.facebook.imagepipeline.animated.testing.MyShadowBitmap; import com.facebook.imagepipeline.animated.testing.MyShadowCanvas; import com.facebook.imagepipeline.animated.testing.TestAnimatedDrawableSupportBackend; import com.facebook.imagepipeline.animated.impl.AnimatedDrawableDiagnosticsNoop; import com.facebook.imagepipeline.testing.TestScheduledExecutorService; import com.nineoldandroids.animation.ValueAnimator; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; import static com.facebook.imagepipeline.animated.testing.TestAnimatedDrawableSupportBackend.pixelValue; import static org.junit.Assert.*; /** * Tests for {@link AnimatedDrawable}. */ @RunWith(RobolectricTestRunner.class) @Config(shadows = {MyShadowCanvas.class, MyShadowBitmap.class}) public class AnimatedDrawableSupportTest { private static final int WIDTH = 200; private static final int HEIGHT = 100; private static final int[] FRAME_DURATIONS = new int[]{ 60, 30, 15, 30, 60 }; private static final int[] FRAME_DURATIONS_LONG = new int[]{ 10000, 10000 }; private FakeClock mFakeClock; private TestScheduledExecutorService mTestScheduledExecutorService; private TestAnimatedDrawableSupportBackend mBackend; private TestAnimatedDrawableCachingBackend mCachingBackend; private MyCallback mCallback; private AnimatedDrawableSupport mDrawable; @Before public void setup() { mFakeClock = new FakeClock(); mTestScheduledExecutorService = new TestScheduledExecutorService(mFakeClock); mBackend = new TestAnimatedDrawableSupportBackend(WIDTH, HEIGHT, FRAME_DURATIONS); mCachingBackend = new TestAnimatedDrawableCachingBackend(mBackend); mCallback = new MyCallback(mFakeClock); mDrawable = new AnimatedDrawableSupport( mTestScheduledExecutorService, mCachingBackend, AnimatedDrawableDiagnosticsNoop.getInstance(), mFakeClock); mDrawable.setCallback(mCallback); } @Test public void testIntrinsicDimensions() { assertEquals(WIDTH, mDrawable.getIntrinsicWidth()); assertEquals(HEIGHT, mDrawable.getIntrinsicHeight()); } @Test public void testValueAnimator() { ValueAnimator valueAnimator = mDrawable.createValueAnimator(); assertEquals(mBackend.getDurationMs(), valueAnimator.getDuration()); assertEquals(ValueAnimator.INFINITE, valueAnimator.getRepeatCount()); } @Test public void testScheduling() { Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); prepareDrawable(); // Spot check a pixel. Should be frame 0. mDrawable.draw(canvas); assertEquals(pixelValue(0, 10, 20), bitmap.getPixel(10, 20)); // Advance just before next frame. mFakeClock.incrementBy(FRAME_DURATIONS[0] - 1); mDrawable.draw(canvas); assertEquals(pixelValue(0, 10, 20), bitmap.getPixel(10, 20)); // Advance just to second frame. mFakeClock.incrementBy(1); mDrawable.draw(canvas); assertEquals(pixelValue(1, 10, 20), bitmap.getPixel(10, 20)); // Advance to the last millisecond of the last frame. mFakeClock.incrementBy(FRAME_DURATIONS[1]); mFakeClock.incrementBy(FRAME_DURATIONS[2]); mFakeClock.incrementBy(FRAME_DURATIONS[3]); mFakeClock.incrementBy(FRAME_DURATIONS[4] - 1); mDrawable.draw(canvas); assertEquals(pixelValue(4, 10, 20), bitmap.getPixel(10, 20)); // Make sure wrapping works. mFakeClock.incrementBy(1); mDrawable.draw(canvas); assertEquals(pixelValue(0, 10, 20), bitmap.getPixel(10, 20)); assertEquals(1, mBackend.getDropCachesCallCount()); } @Test public void testDropCachesAfterDrawTimeout() { Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); prepareDrawable(); // Draw and advance to next frame. mDrawable.draw(canvas); // Make sure caches are dropped after couple seconds without a draw call. assertFalse(mDrawable.isWaitingForDraw()); mFakeClock.incrementBy(60); assertTrue(mDrawable.isWaitingForDraw()); mFakeClock.incrementBy(1940); assertEquals(2, mBackend.getDropCachesCallCount()); } @Test public void testDropCachesAfterNextFrameTimeout() { Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); prepareDrawable(); // Draw and advance to next frame. mDrawable.draw(canvas); // Make sure caches are dropped after couple seconds. assertFalse(mDrawable.isWaitingForDraw()); assertTrue(mDrawable.isWaitingForNextFrame()); mCallback.setDropCallbacks(true); mFakeClock.incrementBy(2000); assertEquals(2, mBackend.getDropCachesCallCount()); } @Test public void testDoNotDropCacheIfFramesAreLongDurationSpecialCase() { mBackend = new TestAnimatedDrawableSupportBackend(WIDTH, HEIGHT, FRAME_DURATIONS_LONG); mCachingBackend = new TestAnimatedDrawableCachingBackend(mBackend); mCallback = new MyCallback(mFakeClock); mDrawable = new AnimatedDrawableSupport( mTestScheduledExecutorService, mCachingBackend, AnimatedDrawableDiagnosticsNoop.getInstance(), mFakeClock); mDrawable.setCallback(mCallback); Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); prepareDrawable(); // Draw and advance to next frame. mDrawable.draw(canvas); // After 10 seconds, it should move to the next frame. It shouldn't drop the caches though // until it's 2 seconds after the invalidate. assertFalse(mDrawable.isWaitingForDraw()); assertTrue(mDrawable.isWaitingForNextFrame()); assertEquals(0, mDrawable.getScheduledFrameNumber()); mFakeClock.incrementBy(10000); assertTrue(mDrawable.isWaitingForDraw()); assertFalse(mDrawable.isWaitingForNextFrame()); assertEquals(1, mDrawable.getScheduledFrameNumber()); assertEquals(1, mBackend.getDropCachesCallCount()); mFakeClock.incrementBy(1000); assertEquals(1, mBackend.getDropCachesCallCount()); mFakeClock.incrementBy(2000); assertEquals(2, mBackend.getDropCachesCallCount()); } private void prepareDrawable() { mDrawable.start(); mFakeClock.incrementBy(0); // Just to trigger the callbacks to run. mDrawable.setBounds(0, 0, 200, 100); assertEquals(1, mBackend.getDropCachesCallCount()); } private static class ScheduledRunnable { final Runnable runnable; final long when; private ScheduledRunnable(Runnable runnable, long when) { this.runnable = runnable; this.when = when; } } private static class TestAnimatedDrawableCachingBackend extends DelegatingAnimatedDrawableBackend implements AnimatedDrawableCachingBackend { public TestAnimatedDrawableCachingBackend(AnimatedDrawableBackend animatedDrawableBackend) { super(animatedDrawableBackend); } @Override public CloseableReference<Bitmap> getBitmapForFrame(int frameNumber) { Bitmap bitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888); getDelegate().renderFrame(frameNumber, new Canvas(bitmap)); return CloseableReference.of( bitmap, new ResourceReleaser<Bitmap>() { @Override public void release(Bitmap value) { } }); } @Override public CloseableReference<Bitmap> getPreviewBitmap() { return null; } @Override public CloseableReference<Bitmap> getPreDecodedFrame(int frameNumber) { return null; } @Override public void appendDebugOptionString(StringBuilder sb) { } @Override public AnimatedDrawableCachingBackend forNewBounds(Rect bounds) { return this; } } private static class MyCallback implements Drawable.Callback { private final FakeClock mFakeClock; private final List<ScheduledRunnable> mScheduledRunnables = new ArrayList<>(); private boolean mDropCallbacks; MyCallback(FakeClock fakeClock) { mFakeClock = fakeClock; mFakeClock.addListener( new FakeClock.OnTickListener() { @Override public void onTick() { runReadyTasks(); } }); } @Override public void invalidateDrawable(Drawable who) { } @Override public void scheduleDrawable(Drawable who, Runnable what, long when) { mScheduledRunnables.add(new ScheduledRunnable(what, when)); } @Override public void unscheduleDrawable(Drawable who, Runnable what) { Iterator<ScheduledRunnable> iterator = mScheduledRunnables.iterator(); while (iterator.hasNext()) { ScheduledRunnable next = iterator.next(); if (next.runnable == what) { iterator.remove(); } } } void runReadyTasks() { List<Runnable> toRun = new ArrayList<>(); long now = mFakeClock.now(); Iterator<ScheduledRunnable> iterator = mScheduledRunnables.iterator(); while (iterator.hasNext()) { ScheduledRunnable next = iterator.next(); if (next.when <= now) { iterator.remove(); toRun.add(next.runnable); } } if (!mDropCallbacks) { for (Runnable runnable : toRun) { runnable.run(); } } } void setDropCallbacks(boolean dropCallbacks) { mDropCallbacks = dropCallbacks; } } }