/*
* 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();
float progress = dataSource.getProgress();
T image = dataSource.getResult();
if (image != null) {
onNewResultInternal(id, dataSource, image, progress, 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);
}
@Override
public void onProgressUpdate(DataSource<T> dataSource) {
boolean isFinished = dataSource.isFinished();
float progress = dataSource.getProgress();
onProgressUpdateInternal(id, dataSource, progress, isFinished);
}
};
mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor);
}
private void onNewResultInternal(
String id,
DataSource<T> dataSource,
@Nullable T image,
float progress,
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, 1f, wasImmediate);
getControllerListener().onFinalImageSet(id, getImageInfo(image), getAnimatable());
// IMPORTANT: do not execute any instance-specific code after this point
} else {
logMessageAndImage("set_intermediate_result @ onNewResult", image);
mSettableDraweeHierarchy.setImage(drawable, progress, wasImmediate);
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 void onProgressUpdateInternal(
String id,
DataSource<T> dataSource,
float progress,
boolean isFinished) {
// ignore late callbacks (data source that failed is not the one we expected)
if (!isExpectedDataSource(id, dataSource)) {
logMessageAndFailure("ignore_old_datasource @ onProgress", null);
dataSource.close();
return;
}
if (!isFinished) {
mSettableDraweeHierarchy.setProgress(progress, false);
}
}
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);
}
}
@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();
}
}