/*
* 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.Set;
import java.util.concurrent.atomic.AtomicLong;
import android.content.Context;
import android.graphics.drawable.Animatable;
import com.facebook.common.internal.Lists;
import com.facebook.common.internal.Objects;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.Supplier;
import com.facebook.datasource.DataSource;
import com.facebook.datasource.DataSources;
import com.facebook.datasource.FirstAvailableDataSourceSupplier;
import com.facebook.datasource.IncreasingQualityDataSourceSupplier;
import com.facebook.drawee.components.RetryManager;
import com.facebook.drawee.interfaces.SimpleDraweeControllerBuilder;
import com.facebook.drawee.gestures.GestureDetector;
import com.facebook.drawee.interfaces.DraweeController;
/**
* Base implementation for Drawee controller builders.
*/
public abstract class AbstractDraweeControllerBuilder <
BUILDER extends AbstractDraweeControllerBuilder<BUILDER, REQUEST, IMAGE, INFO>,
REQUEST,
IMAGE,
INFO>
implements SimpleDraweeControllerBuilder {
private static final ControllerListener<Object> sAutoPlayAnimationsListener =
new BaseControllerListener<Object>() {
@Override
public void onFinalImageSet(String id, @Nullable Object info, @Nullable Animatable anim) {
if (anim != null) {
anim.start();
}
}
};
private static final NullPointerException NO_REQUEST_EXCEPTION =
new NullPointerException("No image request was specified!");
// components
private final Context mContext;
private final Set<ControllerListener> mBoundControllerListeners;
// builder parameters
private @Nullable Object mCallerContext;
private @Nullable REQUEST mImageRequest;
private @Nullable REQUEST mLowResImageRequest;
private @Nullable REQUEST[] mMultiImageRequests;
private @Nullable Supplier<DataSource<IMAGE>> mDataSourceSupplier;
private @Nullable ControllerListener<? super INFO> mControllerListener;
private boolean mTapToRetryEnabled;
private boolean mAutoPlayAnimations;
// old controller to reuse
private @Nullable DraweeController mOldController;
private static final AtomicLong sIdCounter = new AtomicLong();
protected AbstractDraweeControllerBuilder(
Context context,
Set<ControllerListener> boundControllerListeners) {
mContext = context;
mBoundControllerListeners = boundControllerListeners;
init();
}
/** Initializes this builder. */
private void init() {
mCallerContext = null;
mImageRequest = null;
mLowResImageRequest = null;
mMultiImageRequests = null;
mControllerListener = null;
mTapToRetryEnabled = false;
mAutoPlayAnimations = false;
mOldController = null;
}
/** Resets this builder to its initial values making it reusable. */
public BUILDER reset() {
init();
return getThis();
}
/** Sets the caller context. */
@Override
public BUILDER setCallerContext(Object callerContext) {
mCallerContext = callerContext;
return getThis();
}
/** Gets the caller context. */
@Nullable
public Object getCallerContext() {
return mCallerContext;
}
/** Sets the image request. */
public BUILDER setImageRequest(REQUEST imageRequest) {
mImageRequest = imageRequest;
return getThis();
}
/** Gets the image request. */
@Nullable
public REQUEST getImageRequest() {
return mImageRequest;
}
/** Sets the low-res image request. */
public BUILDER setLowResImageRequest(REQUEST lowResImageRequest) {
mLowResImageRequest = lowResImageRequest;
return getThis();
}
/** Gets the low-res image request. */
@Nullable
public REQUEST getLowResImageRequest() {
return mLowResImageRequest;
}
/**
* Sets the array of first-available image requests that will be probed in order.
* <p> For performance reasons, the array is not deep-copied, but only stored by reference.
* Please don't modify once submitted.
*/
public BUILDER setFirstAvailableImageRequests(REQUEST[] firstAvailableImageRequests) {
mMultiImageRequests = firstAvailableImageRequests;
return getThis();
}
/**
* Gets the array of first-available image requests.
* <p> For performance reasons, the array is not deep-copied, but only stored by reference.
* Please don't modify.
*/
@Nullable
public REQUEST[] getFirstAvailableImageRequests() {
return mMultiImageRequests;
}
/**
* Sets the data source supplier to be used.
*
* <p/> Note: This is mutually exclusive with other image request setters.
*/
public void setDataSourceSupplier(@Nullable Supplier<DataSource<IMAGE>> dataSourceSupplier) {
mDataSourceSupplier = dataSourceSupplier;
}
/**
* Gets the data source supplier if set.
*
* <p/>Important: this only returns the externally set data source (if any). Subclasses should
* use {#code obtainDataSourceSupplier()} to obtain a data source to be passed to the controller.
*/
@Nullable
public Supplier<DataSource<IMAGE>> getDataSourceSupplier() {
return mDataSourceSupplier;
}
/** Sets whether tap-to-retry is enabled. */
public BUILDER setTapToRetryEnabled(boolean enabled) {
mTapToRetryEnabled = enabled;
return getThis();
}
/** Gets whether tap-to-retry is enabled. */
public boolean getTapToRetryEnabled() {
return mTapToRetryEnabled;
}
/** Sets whether to auto play animations. */
public BUILDER setAutoPlayAnimations(boolean enabled) {
mAutoPlayAnimations = enabled;
return getThis();
}
/** Gets whether to auto play animations. */
public boolean getAutoPlayAnimations() {
return mAutoPlayAnimations;
}
/** Sets the controller listener. */
public BUILDER setControllerListener(ControllerListener<? super INFO> controllerListener) {
mControllerListener = controllerListener;
return getThis();
}
/** Gets the controller listener */
@Nullable
public ControllerListener<? super INFO> getControllerListener() {
return mControllerListener;
}
/** Sets the old controller to be reused if possible. */
@Override
public BUILDER setOldController(@Nullable DraweeController oldController) {
mOldController = oldController;
return getThis();
}
/** Gets the old controller to be reused. */
@Nullable
public DraweeController getOldController() {
return mOldController;
}
/** Builds the specified controller. */
@Override
public AbstractDraweeController build() {
validate();
// if only a low-res request is specified, treat it as a final request.
if (mImageRequest == null && mMultiImageRequests == null && mLowResImageRequest != null) {
mImageRequest = mLowResImageRequest;
mLowResImageRequest = null;
}
return buildController();
}
/** Validates the parameters before building a controller. */
protected void validate() {
Preconditions.checkState(
(mMultiImageRequests == null) || (mImageRequest == null),
"Cannot specify both ImageRequest and FirstAvailableImageRequests!");
Preconditions.checkState(
(mDataSourceSupplier == null) ||
(mMultiImageRequests == null && mImageRequest == null && mLowResImageRequest == null),
"Cannot specify DataSourceSupplier with other ImageRequests! Use one or the other.");
}
/** Builds a regular controller. */
protected AbstractDraweeController buildController() {
AbstractDraweeController controller = obtainController();
maybeBuildAndSetRetryManager(controller);
maybeAttachListeners(controller);
return controller;
}
/** Generates unique controller id. */
protected static String generateUniqueControllerId() {
return String.valueOf(sIdCounter.getAndIncrement());
}
/** Gets the top-level data source supplier to be used by a controller. */
protected Supplier<DataSource<IMAGE>> obtainDataSourceSupplier() {
if (mDataSourceSupplier != null) {
return mDataSourceSupplier;
}
Supplier<DataSource<IMAGE>> supplier = null;
// final image supplier;
if (mImageRequest != null) {
supplier = getDataSourceSupplierForRequest(mImageRequest);
} else if (mMultiImageRequests != null) {
supplier = getFirstAvailableDataSourceSupplier(mMultiImageRequests);
}
// increasing-quality supplier; highest-quality supplier goes first
if (supplier != null && mLowResImageRequest != null) {
List<Supplier<DataSource<IMAGE>>> suppliers =
Lists.newArrayListWithCapacity(2);
suppliers.add(supplier);
suppliers.add(getDataSourceSupplierForRequest(mLowResImageRequest));
supplier = IncreasingQualityDataSourceSupplier.create(suppliers);
}
// no image requests; use null data source supplier
if (supplier == null) {
supplier = DataSources.getFailedDataSourceSupplier(NO_REQUEST_EXCEPTION);
}
return supplier;
}
protected Supplier<DataSource<IMAGE>> getFirstAvailableDataSourceSupplier(
REQUEST[] imageRequests) {
List<Supplier<DataSource<IMAGE>>> suppliers =
Lists.newArrayListWithCapacity(imageRequests.length * 2);
// we first add cache-only suppliers, then the full-fetch ones
for (int i = 0; i < imageRequests.length; i++) {
suppliers.add(getDataSourceSupplierForRequest(imageRequests[i], /*cacheOnly */ true));
}
for (int i = 0; i < imageRequests.length; i++) {
suppliers.add(getDataSourceSupplierForRequest(imageRequests[i]));
}
return FirstAvailableDataSourceSupplier.create(suppliers);
}
/** Creates a data source supplier for the given image request. */
protected Supplier<DataSource<IMAGE>> getDataSourceSupplierForRequest(REQUEST imageRequest) {
return getDataSourceSupplierForRequest(imageRequest, /* bitmapCacheOnly */ false);
}
/** Creates a data source supplier for the given image request. */
protected Supplier<DataSource<IMAGE>> getDataSourceSupplierForRequest(
final REQUEST imageRequest,
final boolean bitmapCacheOnly) {
final Object callerContext = getCallerContext();
return new Supplier<DataSource<IMAGE>>() {
@Override
public DataSource<IMAGE> get() {
return getDataSourceForRequest(imageRequest, callerContext, bitmapCacheOnly);
}
@Override
public String toString() {
return Objects.toStringHelper(this)
.add("request", imageRequest.toString())
.toString();
}
};
}
/** Attaches listeners (if specified) to the given controller. */
protected void maybeAttachListeners(AbstractDraweeController controller) {
if (mBoundControllerListeners != null) {
for (ControllerListener<? super INFO> listener : mBoundControllerListeners) {
controller.addControllerListener(listener);
}
}
if (mControllerListener != null) {
controller.addControllerListener(mControllerListener);
}
if (mAutoPlayAnimations) {
controller.addControllerListener(sAutoPlayAnimationsListener);
}
}
/** Installs a retry manager (if specified) to the given controller. */
protected void maybeBuildAndSetRetryManager(AbstractDraweeController controller) {
if (!mTapToRetryEnabled) {
return;
}
RetryManager retryManager = controller.getRetryManager();
if (retryManager == null) {
retryManager = new RetryManager();
controller.setRetryManager(retryManager);
}
retryManager.setTapToRetryEnabled(mTapToRetryEnabled);
maybeBuildAndSetGestureDetector(controller);
}
/** Installs a gesture detector to the given controller. */
protected void maybeBuildAndSetGestureDetector(AbstractDraweeController controller) {
GestureDetector gestureDetector = controller.getGestureDetector();
if (gestureDetector == null) {
gestureDetector = GestureDetector.newInstance(mContext);
controller.setGestureDetector(gestureDetector);
}
}
/* Gets the context. */
protected Context getContext() {
return mContext;
}
/** Concrete builder classes should override this method to return a new controller. */
protected abstract AbstractDraweeController obtainController();
/**
* Concrete builder classes should override this method to return a data source for the request.
*
* <p/>IMPORTANT: Do NOT ever call this method directly. This method is only to be called from
* a supplier created in {#code getDataSourceSupplierForRequest(REQUEST, boolean)}.
*
* <p/>IMPORTANT: Make sure that you do NOT use any non-final field from this method, as the field
* may change if the instance of this builder gets reused. If any such field is required, override
* {#code getDataSourceSupplierForRequest(REQUEST, boolean)}, and store the field in a final
* variable (same as it is done for callerContext).
*/
protected abstract DataSource<IMAGE> getDataSourceForRequest(
final REQUEST imageRequest,
final Object callerContext,
final boolean bitmapCacheOnly);
/** Concrete builder classes should override this method to return {#code this}. */
protected abstract BUILDER getThis();
}