/* * 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 javax.annotation.concurrent.NotThreadSafe; import java.util.concurrent.Executor; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import com.facebook.common.internal.Objects; import com.facebook.common.internal.Preconditions; import com.facebook.common.logging.FLog; import com.facebook.drawee.components.DeferredReleaser; import com.facebook.drawee.components.DraweeEventTracker; import com.facebook.drawee.components.RetryManager; import com.facebook.drawee.gestures.GestureDetector; import com.facebook.drawee.interfaces.DraweeHierarchy; import com.facebook.drawee.interfaces.DraweeController; import com.facebook.drawee.interfaces.SettableDraweeHierarchy; import com.facebook.datasource.BaseDataSubscriber; import com.facebook.datasource.DataSource; import com.facebook.datasource.DataSubscriber; import static com.facebook.drawee.components.DraweeEventTracker.Event; /** * Abstract Drawee controller that implements common functionality * regardless of the backend used to fetch the image. * * All methods should be called on the main UI thread. * * @param <T> image type (e.g. Bitmap) * @param <INFO> image info type (can be same as T) */ @NotThreadSafe public abstract class AbstractDraweeController<T, INFO> implements DraweeController, DeferredReleaser.Releasable, GestureDetector.ClickListener { /** * This class is used to allow an optimization of not creating a ForwardingControllerListener * when there is only a single controller listener. */ private static class InternalForwardingListener<INFO> extends ForwardingControllerListener<INFO> { public static <INFO> InternalForwardingListener<INFO> createInternal( ControllerListener<? super INFO> listener1, ControllerListener<? super INFO> listener2) { InternalForwardingListener<INFO> forwarder = new InternalForwardingListener<INFO>(); forwarder.addListener(listener1); forwarder.addListener(listener2); return forwarder; } } private static final Class<?> TAG = AbstractDraweeController.class; // Components private final DraweeEventTracker mEventTracker = new DraweeEventTracker(); private final DeferredReleaser mDeferredReleaser; private final Executor mUiThreadImmediateExecutor; // Optional components private @Nullable RetryManager mRetryManager; private @Nullable GestureDetector mGestureDetector; private @Nullable ControllerListener<INFO> mControllerListener; // Hierarchy private @Nullable SettableDraweeHierarchy mSettableDraweeHierarchy; private @Nullable Drawable mControllerOverlay; // Constant state (non-final because controllers can be reused) private String mId; private Object mCallerContext; // Mutable state private boolean mIsAttached; private boolean mIsRequestSubmitted; private boolean mHasFetchFailed; private @Nullable DataSource<T> mDataSource; private @Nullable T mFetchedImage; private @Nullable Drawable mDrawable; public AbstractDraweeController( DeferredReleaser deferredReleaser, Executor uiThreadImmediateExecutor, String id, Object callerContext) { mDeferredReleaser = deferredReleaser; mUiThreadImmediateExecutor = uiThreadImmediateExecutor; init(id, callerContext); } /** * Initializes this controller with the new id and caller context. * This allows for reusing of the existing controller instead of instantiating a new one. * This method should be called when the controller is in detached state. * @param id unique id for this controller * @param callerContext tag and context for this controller */ protected void initialize(String id, Object callerContext) { init(id, callerContext); } private void init(String id, Object callerContext) { mEventTracker.recordEvent(Event.ON_INIT_CONTROLLER); // cancel deferred release if (mDeferredReleaser != null) { mDeferredReleaser.cancelDeferredRelease(this); } // reinitialize mutable state (fetch state) mIsAttached = false; releaseFetch(); // reinitialize optional components if (mRetryManager != null) { mRetryManager.init(); } if (mGestureDetector != null) { mGestureDetector.init(); mGestureDetector.setClickListener(this); } if (mControllerListener instanceof InternalForwardingListener) { ((InternalForwardingListener) mControllerListener).clearListeners(); } else { mControllerListener = null; } // clear hierarchy and controller overlay if (mSettableDraweeHierarchy != null) { mSettableDraweeHierarchy.reset(); mSettableDraweeHierarchy.setControllerOverlay(null); mSettableDraweeHierarchy = null; } mControllerOverlay = null; // reinitialize constant state if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v(TAG, "controller %x %s -> %s: initialize", System.identityHashCode(this), mId, id); } mId = id; mCallerContext = callerContext; } @Override public void release() { mEventTracker.recordEvent(Event.ON_RELEASE_CONTROLLER); if (mRetryManager != null) { mRetryManager.reset(); } if (mGestureDetector != null) { mGestureDetector.reset(); } if (mSettableDraweeHierarchy != null) { mSettableDraweeHierarchy.reset(); } releaseFetch(); } private void releaseFetch() { boolean wasRequestSubmitted = mIsRequestSubmitted; mIsRequestSubmitted = false; mHasFetchFailed = false; if (mDataSource != null) { mDataSource.close(); mDataSource = null; } if (mDrawable != null) { releaseDrawable(mDrawable); } mDrawable = null; if (mFetchedImage != null) { logMessageAndImage("release", mFetchedImage); releaseImage(mFetchedImage); mFetchedImage = null; } if (wasRequestSubmitted) { getControllerListener().onRelease(mId); } } /** Gets the controller id. */ public String getId() { return mId; } /** Gets the analytic tag & caller context */ public Object getCallerContext() { return mCallerContext; } /** Gets retry manager. */ protected @Nullable RetryManager getRetryManager() { return mRetryManager; } /** Sets retry manager. */ protected void setRetryManager(@Nullable RetryManager retryManager) { mRetryManager = retryManager; } /** Gets gesture detector. */ protected @Nullable GestureDetector getGestureDetector() { return mGestureDetector; } /** Sets gesture detector. */ protected void setGestureDetector(@Nullable GestureDetector gestureDetector) { mGestureDetector = gestureDetector; if (mGestureDetector != null) { mGestureDetector.setClickListener(this); } } /** Adds controller listener. */ public void addControllerListener(ControllerListener<? super INFO> controllerListener) { Preconditions.checkNotNull(controllerListener); if (mControllerListener instanceof InternalForwardingListener) { ((InternalForwardingListener<INFO>) mControllerListener).addListener(controllerListener); return; } if (mControllerListener != null) { mControllerListener = InternalForwardingListener.createInternal( mControllerListener, controllerListener); return; } // Listener only receives <INFO>, it never produces one. // That means if it can accept <? super INFO>, it can very well accept <INFO>. mControllerListener = (ControllerListener<INFO>) controllerListener; } /** Removes controller listener. */ public void removeControllerListener(ControllerListener<? super INFO> controllerListener) { Preconditions.checkNotNull(controllerListener); if (mControllerListener instanceof InternalForwardingListener) { ((InternalForwardingListener<INFO>) mControllerListener).removeListener(controllerListener); return; } if (mControllerListener == controllerListener) { mControllerListener = null; } } /** Gets controller listener for internal use. */ protected ControllerListener<INFO> getControllerListener() { if (mControllerListener == null) { return BaseControllerListener.getNoOpListener(); } return mControllerListener; } /** Gets the hierarchy */ @Override public @Nullable DraweeHierarchy getHierarchy() { return mSettableDraweeHierarchy; } /** * Sets the hierarchy. * * <p>The controller should be detached when this method is called. * @param hierarchy This must be an instance of {@link SettableDraweeHierarchy} */ @Override public void setHierarchy(@Nullable DraweeHierarchy hierarchy) { if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v( TAG, "controller %x %s: setHierarchy: %s", System.identityHashCode(this), mId, hierarchy); } mEventTracker.recordEvent( (hierarchy != null) ? Event.ON_SET_HIERARCHY : Event.ON_CLEAR_HIERARCHY); // force release in case request was submitted if (mIsRequestSubmitted) { mDeferredReleaser.cancelDeferredRelease(this); release(); } // clear the existing hierarchy if (mSettableDraweeHierarchy != null) { mSettableDraweeHierarchy.setControllerOverlay(null); mSettableDraweeHierarchy = null; } // set the new hierarchy if (hierarchy != null) { Preconditions.checkArgument(hierarchy instanceof SettableDraweeHierarchy); mSettableDraweeHierarchy = (SettableDraweeHierarchy) hierarchy; mSettableDraweeHierarchy.setControllerOverlay(mControllerOverlay); } } /** Sets the controller overlay */ protected void setControllerOverlay(@Nullable Drawable controllerOverlay) { mControllerOverlay = controllerOverlay; if (mSettableDraweeHierarchy != null) { mSettableDraweeHierarchy.setControllerOverlay(mControllerOverlay); } } /** Gets the controller overlay */ protected @Nullable Drawable getControllerOverlay() { return mControllerOverlay; } @Override public void onAttach() { if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v( TAG, "controller %x %s: onAttach: %s", System.identityHashCode(this), mId, mIsRequestSubmitted ? "request already submitted" : "request needs submit"); } mEventTracker.recordEvent(Event.ON_ATTACH_CONTROLLER); Preconditions.checkNotNull(mSettableDraweeHierarchy); mDeferredReleaser.cancelDeferredRelease(this); mIsAttached = true; if (!mIsRequestSubmitted) { submitRequest(); } } @Override public void onDetach() { if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v(TAG, "controller %x %s: onDetach", System.identityHashCode(this), mId); } mEventTracker.recordEvent(Event.ON_DETACH_CONTROLLER); mIsAttached = false; mDeferredReleaser.scheduleDeferredRelease(this); } @Override public boolean onTouchEvent(MotionEvent event) { if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v(TAG, "controller %x %s: onTouchEvent %s", System.identityHashCode(this), mId, event); } if (mGestureDetector == null) { return false; } if (mGestureDetector.isCapturingGesture() || shouldHandleGesture()) { mGestureDetector.onTouchEvent(event); return true; } return false; } /** Returns whether the gesture should be handled by the controller */ protected boolean shouldHandleGesture() { return shouldRetryOnTap(); } private boolean shouldRetryOnTap() { // We should only handle touch event if we are expecting some gesture. // For example, we expect click when fetch fails and tap-to-retry is enabled. return mHasFetchFailed && mRetryManager != null && mRetryManager.shouldRetryOnTap(); } @Override public boolean onClick() { if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v(TAG, "controller %x %s: onClick", System.identityHashCode(this), mId); } if (shouldRetryOnTap()) { mRetryManager.notifyTapToRetry(); mSettableDraweeHierarchy.reset(); submitRequest(); return true; } return false; } protected void submitRequest() { mEventTracker.recordEvent(Event.ON_DATASOURCE_SUBMIT); getControllerListener().onSubmit(mId, mCallerContext); mSettableDraweeHierarchy.setProgress(0, true); mIsRequestSubmitted = true; mHasFetchFailed = false; mDataSource = getDataSource(); if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v( TAG, "controller %x %s: submitRequest: dataSource: %x", System.identityHashCode(this), mId, System.identityHashCode(mDataSource)); } final String id = mId; final boolean wasImmediate = mDataSource.hasResult(); final DataSubscriber<T> dataSubscriber = new BaseDataSubscriber<T>() { @Override public void onNewResultImpl(DataSource<T> dataSource) { // isFinished must be obtained before image, otherwise we might set intermediate result // as final image. boolean isFinished = dataSource.isFinished(); T image = dataSource.getResult(); if (image != null) { onNewResultInternal(id, dataSource, image, isFinished, wasImmediate); } else if (isFinished) { onFailureInternal(id, dataSource, new NullPointerException(), /* isFinished */ true); } } @Override public void onFailureImpl(DataSource<T> dataSource) { onFailureInternal(id, dataSource, dataSource.getFailureCause(), /* isFinished */ true); } }; mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor); } private void onNewResultInternal( String id, DataSource<T> dataSource, @Nullable T image, boolean isFinished, boolean wasImmediate) { // ignore late callbacks (data source that returned the new result is not the one we expected) if (!isExpectedDataSource(id, dataSource)) { logMessageAndImage("ignore_old_datasource @ onNewResult", image); releaseImage(image); dataSource.close(); return; } mEventTracker.recordEvent( isFinished ? Event.ON_DATASOURCE_RESULT : Event.ON_DATASOURCE_RESULT_INT); // create drawable Drawable drawable; try { drawable = createDrawable(image); } catch (Exception exception) { logMessageAndImage("drawable_failed @ onNewResult", image); releaseImage(image); onFailureInternal(id, dataSource, exception, isFinished); return; } T previousImage = mFetchedImage; Drawable previousDrawable = mDrawable; mFetchedImage = image; mDrawable = drawable; try { // set the new image if (isFinished) { logMessageAndImage("set_final_result @ onNewResult", image); mDataSource = null; mSettableDraweeHierarchy.setImage(drawable, wasImmediate, 100); getControllerListener().onFinalImageSet(id, getImageInfo(image), getAnimatable()); // IMPORTANT: do not execute any instance-specific code after this point } else { logMessageAndImage("set_intermediate_result @ onNewResult", image); int progress = getProgress(dataSource, image); mSettableDraweeHierarchy.setImage(drawable, wasImmediate, progress); getControllerListener().onIntermediateImageSet(id, getImageInfo(image)); // IMPORTANT: do not execute any instance-specific code after this point } } finally { if (previousDrawable != null && previousDrawable != drawable) { releaseDrawable(previousDrawable); } if (previousImage != null && previousImage != image) { logMessageAndImage("release_previous_result @ onNewResult", previousImage); releaseImage(previousImage); } } } private void onFailureInternal( String id, DataSource<T> dataSource, Throwable throwable, boolean isFinished) { // ignore late callbacks (data source that failed is not the one we expected) if (!isExpectedDataSource(id, dataSource)) { logMessageAndFailure("ignore_old_datasource @ onFailure", throwable); dataSource.close(); return; } mEventTracker.recordEvent( isFinished ? Event.ON_DATASOURCE_FAILURE : Event.ON_DATASOURCE_FAILURE_INT); // fail only if the data source is finished if (isFinished) { logMessageAndFailure("final_failed @ onFailure", throwable); mDataSource = null; mHasFetchFailed = true; if (shouldRetryOnTap()) { mSettableDraweeHierarchy.setRetry(throwable); } else { mSettableDraweeHierarchy.setFailure(throwable); } getControllerListener().onFailure(mId, throwable); // IMPORTANT: do not execute any instance-specific code after this point } else { logMessageAndFailure("intermediate_failed @ onFailure", throwable); getControllerListener().onIntermediateImageFailed(mId, throwable); // IMPORTANT: do not execute any instance-specific code after this point } } private boolean isExpectedDataSource(String id, DataSource<T> dataSource) { // There are several situations in which an old data source might return a result that we are no // longer interested in. To verify that the result is indeed expected, we check several things: return id.equals(mId) && dataSource == mDataSource && mIsRequestSubmitted; } private void logMessageAndImage(String messageAndMethod, T image) { if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v( TAG, "controller %x %s: %s: image: %s %x", System.identityHashCode(this), mId, messageAndMethod, getImageClass(image), getImageHash(image)); } } private void logMessageAndFailure(String messageAndMethod, Throwable throwable) { if (FLog.isLoggable(FLog.VERBOSE)) { FLog.v( TAG, "controller %x %s: %s: failure: %s", System.identityHashCode(this), mId, messageAndMethod, throwable); } } protected int getProgress(DataSource<T> dataSource, T image) { // IMPORTANT: At the moment, GenericDraweeHierarchy hides progressbar only when progress // reaches 100. Once we implement more accurate estimate, we will have to explicitly specify // whether or not should the progressbar be visible. return dataSource.isFinished() ? 100 : 50; } @Override public @Nullable Animatable getAnimatable() { return (mDrawable instanceof Animatable) ? (Animatable) mDrawable : null; } protected abstract DataSource<T> getDataSource(); protected abstract Drawable createDrawable(T image); protected abstract @Nullable INFO getImageInfo(T image); protected String getImageClass(@Nullable T image) { return (image != null) ? image.getClass().getSimpleName() : "<null>"; } protected int getImageHash(@Nullable T image) { return System.identityHashCode(image); } protected abstract void releaseImage(@Nullable T image); protected abstract void releaseDrawable(@Nullable Drawable drawable); @Override public String toString() { return Objects.toStringHelper(this) .add("isAttached", mIsAttached) .add("isRequestSubmitted", mIsRequestSubmitted) .add("hasFetchFailed", mHasFetchFailed) .add("fetchedImage", getImageHash(mFetchedImage)) .add("events", mEventTracker.toString()) .toString(); } }