/* * 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.drawee.controller; import javax.annotation.Nullable; import java.util.List; import java.util.concurrent.Executor; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.internal.Lists; import com.facebook.common.internal.Supplier; import com.facebook.common.internal.Throwables; import com.facebook.datasource.DataSource; import com.facebook.datasource.SettableDataSource; import com.facebook.drawee.components.DeferredReleaser; import com.facebook.drawee.interfaces.SettableDraweeHierarchy; import com.facebook.testing.robolectric.v2.WithTestDefaultsRunner; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import static org.junit.Assert.*; import static org.mockito.Mockito.*; /** * Tests for AbstractDraweeController */ @RunWith(WithTestDefaultsRunner.class) public class AbstractDraweeControllerTest { public static class FakeImageInfo { } public static class FakeImage { private final Drawable mDrawable; private final FakeImageInfo mImageInfo; private boolean mIsOpened; private boolean mIsClosed; protected FakeImage(Drawable drawable, FakeImageInfo imageInfo) { mDrawable = drawable; mImageInfo = imageInfo; mIsOpened = false; mIsClosed = false; } public Drawable getDrawable() { return mDrawable; } public @Nullable FakeImageInfo getImageInfo() { return mImageInfo; } public void open() { mIsOpened = true; } public boolean isOpened() { return mIsOpened; } public void close() { mIsClosed = true; } public boolean isClosed() { return mIsClosed; } public static FakeImage create(Drawable drawable) { return new FakeImage(drawable, null); } public static FakeImage create(Drawable drawable, FakeImageInfo imageInfo) { return new FakeImage(drawable, imageInfo); } } public static class FakeDraweeController extends AbstractDraweeController<FakeImage, FakeImageInfo> { private Supplier<DataSource<FakeImage>> mDataSourceSupplier; public boolean mIsAttached = false; public FakeDraweeController( DeferredReleaser deferredReleaser, Executor uiThreadExecutor, Supplier<DataSource<FakeImage>> dataSourceSupplier, String id, Object callerContext) { super(deferredReleaser, uiThreadExecutor, id, callerContext); mDataSourceSupplier = dataSourceSupplier; } @Override public void onAttach() { mIsAttached = true; super.onAttach(); } @Override public void onDetach() { mIsAttached = false; super.onDetach(); } public boolean isAttached() { return mIsAttached; } @Override protected DataSource<FakeImage> getDataSource() { return mDataSourceSupplier.get(); } @Override protected Drawable createDrawable(FakeImage image) { return image.getDrawable(); } @Override protected @Nullable FakeImageInfo getImageInfo(FakeImage image) { return image.getImageInfo(); } @Override protected void releaseImage(@Nullable FakeImage image) { if (image != null) { image.close(); } } @Override protected void releaseDrawable(@Nullable Drawable drawable) { } } private DeferredReleaser mDeferredReleaser; private Object mCallerContext; private Supplier<DataSource<FakeImage>> mDataSourceSupplier; private SettableDraweeHierarchy mDraweeHierarchy; private Executor mUiThreadExecutor; private FakeDraweeController mController; @Before public void setUp() { mDeferredReleaser = mock(DeferredReleaser.class); mCallerContext = mock(Object.class); mDataSourceSupplier = mock(Supplier.class); mDraweeHierarchy = mock(SettableDraweeHierarchy.class); mUiThreadExecutor = CallerThreadExecutor.getInstance(); mController = new FakeDraweeController( mDeferredReleaser, mUiThreadExecutor, mDataSourceSupplier, "id", mCallerContext); doAnswer( new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { ((DeferredReleaser.Releasable) invocation.getArguments()[0]).release(); return null; } }).when(mDeferredReleaser).scheduleDeferredRelease(any(DeferredReleaser.Releasable.class)); when(mDataSourceSupplier.get()).thenReturn(SettableDataSource.<FakeImage>create()); } @Test public void testOnAttach() { mController.setHierarchy(mDraweeHierarchy); mController.onAttach(); verify(mDeferredReleaser, atLeastOnce()).cancelDeferredRelease(eq(mController)); verify(mDataSourceSupplier).get(); } @Test public void testOnAttach_ThrowsWithoutHierarchy() { try { mController.setHierarchy(null); mController.onAttach(); fail("onAttach should fail if no drawee hierarchy is set!"); } catch (NullPointerException npe) { // expected } } @Test public void testOnDetach() { mController.setHierarchy(mDraweeHierarchy); mController.onAttach(); mController.onDetach(); assertSame(mDraweeHierarchy, mController.getHierarchy()); verify(mDeferredReleaser).scheduleDeferredRelease(mController); } @Test public void testSettingControllerOverlay() { Drawable controllerOverlay1 = mock(Drawable.class); Drawable controllerOverlay2 = mock(Drawable.class); SettableDraweeHierarchy draweeHierarchy1 = mock(SettableDraweeHierarchy.class); SettableDraweeHierarchy draweeHierarchy2 = mock(SettableDraweeHierarchy.class); InOrder inOrder = inOrder(draweeHierarchy1, draweeHierarchy2); // initial state assertNull(mController.getHierarchy()); // set controller overlay before hierarchy mController.setControllerOverlay(controllerOverlay1); // set drawee hierarchy mController.setHierarchy(draweeHierarchy1); assertSame(draweeHierarchy1, mController.getHierarchy()); inOrder.verify(draweeHierarchy1, times(1)).setControllerOverlay(controllerOverlay1); inOrder.verify(draweeHierarchy1, times(0)).reset(); // change drawee hierarchy mController.setHierarchy(draweeHierarchy2); assertSame(draweeHierarchy2, mController.getHierarchy()); inOrder.verify(draweeHierarchy1, times(1)).setControllerOverlay(null); inOrder.verify(draweeHierarchy1, times(0)).reset(); inOrder.verify(draweeHierarchy2, times(1)).setControllerOverlay(controllerOverlay1); inOrder.verify(draweeHierarchy2, times(0)).reset(); // clear drawee hierarchy mController.setHierarchy(null); assertSame(null, mController.getHierarchy()); inOrder.verify(draweeHierarchy1, times(0)).setControllerOverlay(any(Drawable.class)); inOrder.verify(draweeHierarchy1, times(0)).reset(); inOrder.verify(draweeHierarchy2, times(1)).setControllerOverlay(null); inOrder.verify(draweeHierarchy2, times(0)).reset(); // set drawee hierarchy mController.setHierarchy(draweeHierarchy1); assertSame(draweeHierarchy1, mController.getHierarchy()); inOrder.verify(draweeHierarchy1, times(1)).setControllerOverlay(controllerOverlay1); inOrder.verify(draweeHierarchy1, times(0)).reset(); inOrder.verify(draweeHierarchy2, times(0)).setControllerOverlay(any(Drawable.class)); inOrder.verify(draweeHierarchy2, times(0)).reset(); // change controller overlay mController.setControllerOverlay(controllerOverlay2); inOrder.verify(draweeHierarchy1, times(1)).setControllerOverlay(controllerOverlay2); inOrder.verify(draweeHierarchy1, times(0)).reset(); inOrder.verify(draweeHierarchy2, times(0)).setControllerOverlay(any(Drawable.class)); inOrder.verify(draweeHierarchy2, times(0)).reset(); // clear controller overlay mController.setControllerOverlay(null); inOrder.verify(draweeHierarchy1, times(1)).setControllerOverlay(null); inOrder.verify(draweeHierarchy1, times(0)).reset(); inOrder.verify(draweeHierarchy2, times(0)).setControllerOverlay(any(Drawable.class)); inOrder.verify(draweeHierarchy2, times(0)).reset(); } @Test public void testListeners() { ControllerListener<FakeImageInfo> listener1 = mock(ControllerListener.class); ControllerListener<Object> listener2 = mock(ControllerListener.class); InOrder inOrder = inOrder(listener1, listener2); mController.getControllerListener().onRelease("id"); inOrder.verify(listener1, never()).onRelease(anyString()); inOrder.verify(listener2, never()).onRelease(anyString()); mController.addControllerListener(listener1); mController.getControllerListener().onRelease("id"); inOrder.verify(listener1, times(1)).onRelease("id"); inOrder.verify(listener2, never()).onRelease(anyString()); mController.addControllerListener(listener2); mController.getControllerListener().onRelease("id"); inOrder.verify(listener1, times(1)).onRelease("id"); inOrder.verify(listener2, times(1)).onRelease("id"); mController.removeControllerListener(listener1); mController.getControllerListener().onRelease("id"); inOrder.verify(listener1, never()).onRelease(anyString()); inOrder.verify(listener2, times(1)).onRelease("id"); mController.removeControllerListener(listener2); mController.getControllerListener().onRelease("id"); inOrder.verify(listener1, never()).onRelease(anyString()); inOrder.verify(listener2, never()).onRelease(anyString()); } @Test public void testListenerReentrancy_AfterIntermediateSet() { testListenerReentrancy(INTERMEDIATE_FAILURE); } @Test public void testListenerReentrancy_AfterIntermediateFailed() { testListenerReentrancy(INTERMEDIATE_GOOD); } @Test public void testListenerReentrancy_AfterFinalSet() { testListenerReentrancy(SUCCESS); } @Test public void testListenerReentrancy_AfterFailure() { testListenerReentrancy(FAILURE); } private void testListenerReentrancy(int outcome) { final SettableDataSource<FakeImage> dataSource0 = SettableDataSource.create(); final SettableDataSource<FakeImage> dataSource = SettableDataSource.create(); when(mDataSourceSupplier.get()).thenReturn(dataSource0); FakeImage image0 = FakeImage.create(mock(Drawable.class), mock(FakeImageInfo.class)); finish(dataSource0, image0, outcome); ControllerListener listener = new BaseControllerListener<FakeImageInfo>() { @Override public void onIntermediateImageSet(String id, @Nullable FakeImageInfo imageInfo) { initializeAndAttachController("id_AfterIntermediateSet", dataSource); } @Override public void onIntermediateImageFailed(String id, Throwable throwable) { initializeAndAttachController("id_AfterIntermediateFailed", dataSource); } @Override public void onFinalImageSet( String id, @Nullable FakeImageInfo imageInfo, @Nullable Animatable animatable) { initializeAndAttachController("id_AfterFinalSet", dataSource); } @Override public void onFailure(String id, Throwable throwable) { initializeAndAttachController("id_AfterFailure", dataSource); } }; mController.addControllerListener(listener); mController.setHierarchy(mDraweeHierarchy); mController.onAttach(); switch (outcome) { case INTERMEDIATE_GOOD: verifyDhInteraction(SET_IMAGE_P50, image0.getDrawable(), true); Assert.assertEquals("id_AfterIntermediateSet", mController.getId()); break; case INTERMEDIATE_FAILURE: verifyDhInteraction(IGNORE, image0.getDrawable(), true); Assert.assertEquals("id_AfterIntermediateFailed", mController.getId()); break; case SUCCESS: verifyDhInteraction(SET_IMAGE_P100, image0.getDrawable(), true); Assert.assertEquals("id_AfterFinalSet", mController.getId()); break; case FAILURE: verifyDhInteraction(SET_FAILURE, image0.getDrawable(), true); Assert.assertEquals("id_AfterFailure", mController.getId()); break; } verify(mDraweeHierarchy).reset(); FakeImage image = FakeImage.create(mock(Drawable.class), mock(FakeImageInfo.class)); finish(dataSource, image, SUCCESS); verifyDhInteraction(SET_IMAGE_P100, image.getDrawable(), false); } private void initializeAndAttachController(String id, DataSource<FakeImage> dataSource) { try { when(mDataSourceSupplier.get()).thenReturn(dataSource); mController.initialize(id, mCallerContext); mController.setHierarchy(mDraweeHierarchy); mController.onAttach(); } catch (Throwable throwable) { System.err.println( "Exception thrown in listener: " + Throwables.getStackTraceAsString(throwable)); } } @Test public void testLoading1_DelayedSuccess() { testLoading(false, SUCCESS, SET_IMAGE_P100); } @Test public void testLoading1_DelayedFailure() { testLoading(false, FAILURE, SET_FAILURE); } @Test public void testLoading1_ImmediateSuccess() { testLoading(true, SUCCESS, SET_IMAGE_P100); } @Test public void testLoading1_ImmediateFailure() { testLoading(true, FAILURE, SET_FAILURE); } @Test public void testLoadingS_S() { testStreamedLoading( new int[]{SUCCESS}, new int[]{SET_IMAGE_P100}); } @Test public void testLoadingS_F() { testStreamedLoading( new int[]{FAILURE}, new int[]{SET_FAILURE}); } @Test public void testLoadingS_LS() { testStreamedLoading( new int[]{INTERMEDIATE_LOW, SUCCESS}, new int[]{SET_IMAGE_P50, SET_IMAGE_P100}); } @Test public void testLoadingS_GS() { testStreamedLoading( new int[]{INTERMEDIATE_GOOD, SUCCESS}, new int[]{SET_IMAGE_P50, SET_IMAGE_P100}); } @Test public void testLoadingS_FS() { testStreamedLoading( new int[]{INTERMEDIATE_FAILURE, SUCCESS}, new int[]{IGNORE, SET_IMAGE_P100}); } @Test public void testLoadingS_LF() { testStreamedLoading( new int[]{INTERMEDIATE_LOW, FAILURE}, new int[]{SET_IMAGE_P50, SET_FAILURE}); } @Test public void testLoadingS_GF() { testStreamedLoading( new int[]{INTERMEDIATE_GOOD, FAILURE}, new int[]{SET_IMAGE_P50, SET_FAILURE}); } @Test public void testLoadingS_FF() { testStreamedLoading( new int[]{INTERMEDIATE_FAILURE, FAILURE}, new int[]{IGNORE, SET_FAILURE}); } @Test public void testLoadingS_LLS() { testStreamedLoading( new int[]{INTERMEDIATE_LOW, INTERMEDIATE_LOW, SUCCESS}, new int[]{SET_IMAGE_P50, SET_IMAGE_P50, SET_IMAGE_P100}); } @Test public void testLoadingS_FLS() { testStreamedLoading( new int[]{INTERMEDIATE_FAILURE, INTERMEDIATE_LOW, SUCCESS}, new int[]{IGNORE, SET_IMAGE_P20, SET_IMAGE_P100}); } @Test public void testLoadingS_LGS() { testStreamedLoading( new int[]{INTERMEDIATE_LOW, INTERMEDIATE_GOOD, SUCCESS}, new int[]{SET_IMAGE_P50, SET_IMAGE_P50, SET_IMAGE_P100}); } @Test public void testLoadingS_GGS() { testStreamedLoading( 0, new int[]{INTERMEDIATE_GOOD, INTERMEDIATE_GOOD, SUCCESS}, new int[]{SET_IMAGE_P50, SET_IMAGE_P50, SET_IMAGE_P100}); } @Test public void testLoadingS_FGS() { testStreamedLoading( new int[]{INTERMEDIATE_FAILURE, INTERMEDIATE_GOOD, SUCCESS}, new int[]{IGNORE, SET_IMAGE_P50, SET_IMAGE_P100}); } @Test public void testLoadingS_LFS() { testStreamedLoading( new int[]{INTERMEDIATE_LOW, INTERMEDIATE_FAILURE, SUCCESS}, new int[]{SET_IMAGE_P20, IGNORE, SET_IMAGE_P100}); } @Test public void testLoadingS_GFS() { testStreamedLoading( new int[]{INTERMEDIATE_GOOD, INTERMEDIATE_FAILURE, SUCCESS}, new int[]{SET_IMAGE_P50, IGNORE, SET_IMAGE_P100}); } @Test public void testLoadingS_FFS() { testStreamedLoading( new int[]{INTERMEDIATE_FAILURE, INTERMEDIATE_FAILURE, SUCCESS}, new int[]{IGNORE, IGNORE, SET_IMAGE_P100}); } /** * Tests a single loading scenario. * @param isImmediate whether the result is immediate or not * @param outcome outcomes of the submitted request * @param dhInteraction expected interaction with drawee hierarchy after the request finishes */ private void testLoading(boolean isImmediate, int outcome, int dhInteraction) { FakeDraweeController controller = new FakeDraweeController( mDeferredReleaser, mUiThreadExecutor, mDataSourceSupplier, "id2", mCallerContext); // create image and the corresponding data source FakeImage image = FakeImage.create(mock(Drawable.class), mock(FakeImageInfo.class)); SettableDataSource<FakeImage> dataSource = SettableDataSource.create(); when(mDataSourceSupplier.get()).thenReturn(dataSource); // finish immediate if (isImmediate) { finish(dataSource, image, outcome); } // attach controller.setHierarchy(mDraweeHierarchy); controller.onAttach(); // finish delayed if (!isImmediate) { finish(dataSource, image, outcome); } // verify verify(mDataSourceSupplier).get(); verifyDhInteraction(dhInteraction, image.getDrawable(), isImmediate); assertTrue(dataSource.isClosed()); // detach controller.onDetach(); // verify that all open images has been closed assertTrue(image.isOpened() == image.isClosed()); verifyNoMoreInteractions(mDataSourceSupplier); } /** * Tests a suite of loading scenarios with streaming. * @param outcomes outcomes of submitted requests * @param dhInteraction expected interaction with drawee hierarchy after each request finishes */ private void testStreamedLoading(int[] outcomes, int[] dhInteraction) { for (int numImmediate = 0; numImmediate <= 1; numImmediate++) { reset(mDataSourceSupplier, mDraweeHierarchy); System.out.println("numImmediate: " + numImmediate); testStreamedLoading(numImmediate, outcomes, dhInteraction); } } /** * Tests a single loading scenario with streaming. * @param numImmediate number of immediate results * @param outcomes outcomes of submitted requests * @param dhInteraction expected interaction with drawee hierarchy after each request finishes */ private void testStreamedLoading(int numImmediate, int[] outcomes, int[] dhInteraction) { FakeDraweeController controller = new FakeDraweeController( mDeferredReleaser, mUiThreadExecutor, mDataSourceSupplier, "id_streamed", mCallerContext); int n = outcomes.length; // create data source and images SettableDataSource<FakeImage> dataSource = SettableDataSource.create(); when(mDataSourceSupplier.get()).thenReturn(dataSource); List<FakeImage> images = Lists.newArrayList(); for (int i = 0; i < n; i++) { images.add(FakeImage.create(mock(Drawable.class), mock(FakeImageInfo.class))); } // finish immediate for (int i = 0; i < numImmediate; i++) { finish(dataSource, images.get(i), outcomes[i]); } // attach controller.setHierarchy(mDraweeHierarchy); controller.onAttach(); verify(mDraweeHierarchy).setProgress(0, true); // finish delayed for (int i = numImmediate; i < n; i++) { finish(dataSource, images.get(i), outcomes[i]); } // verify verify(mDataSourceSupplier).get(); for (int i = 0; i < n; i++) { verifyDhInteraction(dhInteraction[i], images.get(i).getDrawable(), 0 < numImmediate); } assertTrue(dataSource.isClosed()); // detach controller.onDetach(); // verify that all open images has been closed for (int i = 0; i < n; i++) { assertTrue(images.get(i).isOpened() == images.get(i).isClosed()); } verifyNoMoreInteractions(mDataSourceSupplier); } private void finish(SettableDataSource<FakeImage> dataSource, FakeImage image, int outcome) { switch (outcome) { case FAILURE: dataSource.setFailure(new RuntimeException()); break; case SUCCESS: image.open(); dataSource.setResult(image); break; case INTERMEDIATE_FAILURE: dataSource.setResult(createFaultyImage(), false); break; case INTERMEDIATE_LOW: image.open(); dataSource.setResult(image, false); break; case INTERMEDIATE_GOOD: image.open(); dataSource.setResult(image, false); break; default: throw new UnsupportedOperationException("Unsupported outcome: " + outcome); } } private void verifyDhInteraction(int dhInteraction, Drawable drawable, boolean wasImmediate) { switch (dhInteraction) { case IGNORE: verify(mDraweeHierarchy, never()).setImage(eq(drawable), anyBoolean(), anyInt()); break; case SET_IMAGE_P20: verify(mDraweeHierarchy).setImage(eq(drawable), eq(wasImmediate), eq(50)); break; case SET_IMAGE_P50: verify(mDraweeHierarchy).setImage(eq(drawable), eq(wasImmediate), eq(50)); break; case SET_IMAGE_P100: verify(mDraweeHierarchy).setImage(eq(drawable), eq(wasImmediate), eq(100)); break; case SET_FAILURE: verify(mDraweeHierarchy).setFailure(any(Throwable.class)); break; case SET_RETRY: verify(mDraweeHierarchy).setRetry(any(Throwable.class)); break; default: fail(); break; } } private static final int FAILURE = 0; private static final int SUCCESS = 1; private static final int INTERMEDIATE_FAILURE = 2; private static final int INTERMEDIATE_LOW = 3; private static final int INTERMEDIATE_GOOD = 4; private static final int IGNORE = 1000; private static final int SET_FAILURE = 1001; private static final int SET_RETRY = 1002; private static final int SET_IMAGE_P20 = 1003; private static final int SET_IMAGE_P50 = 1004; private static final int SET_IMAGE_P100 = 1005; private static FakeImage createFaultyImage() { return new FakeImage(null, null) { @Override public Drawable getDrawable() { throw new RuntimeException("Faulty intermediate image"); } }; } }