package com.bumptech.glide.request; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.support.v4.content.res.ResourcesCompat; import android.support.v4.util.Pools; import android.util.Log; import com.bumptech.glide.GlideContext; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.engine.Engine; import com.bumptech.glide.load.engine.GlideException; import com.bumptech.glide.load.engine.Resource; import com.bumptech.glide.request.target.SizeReadyCallback; import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.request.transition.TransitionFactory; import com.bumptech.glide.util.LogTime; import com.bumptech.glide.util.Synthetic; import com.bumptech.glide.util.Util; import com.bumptech.glide.util.pool.FactoryPools; import com.bumptech.glide.util.pool.StateVerifier; /** * A {@link Request} that loads a {@link com.bumptech.glide.load.engine.Resource} into a given * {@link Target}. * * @param <R> The type of the resource that will be transcoded from the loaded resource. */ public final class SingleRequest<R> implements Request, SizeReadyCallback, ResourceCallback, FactoryPools.Poolable { /** Tag for logging internal events, not generally suitable for public use. */ private static final String TAG = "Request"; /** Tag for logging externally useful events (request completion, timing etc). */ private static final String GLIDE_TAG = "Glide"; private static final Pools.Pool<SingleRequest<?>> POOL = FactoryPools.simple(150, new FactoryPools.Factory<SingleRequest<?>>() { @Override public SingleRequest<?> create() { return new SingleRequest<Object>(); } }); private enum Status { /** * Created but not yet running. */ PENDING, /** * In the process of fetching media. */ RUNNING, /** * Waiting for a callback given to the Target to be called to determine target dimensions. */ WAITING_FOR_SIZE, /** * Finished loading media successfully. */ COMPLETE, /** * Failed to load media, may be restarted. */ FAILED, /** * Cancelled by the user, may not be restarted. */ CANCELLED, /** * Cleared by the user with a placeholder set, may not be restarted. */ CLEARED, /** * Temporarily paused by the system, may be restarted. */ PAUSED, } private final String tag = String.valueOf(hashCode()); private final StateVerifier stateVerifier = StateVerifier.newInstance(); private RequestCoordinator requestCoordinator; private GlideContext glideContext; private Object model; private Class<R> transcodeClass; private RequestOptions requestOptions; private int overrideWidth; private int overrideHeight; private Priority priority; private Target<R> target; private RequestListener<R> requestListener; private Engine engine; private TransitionFactory<? super R> animationFactory; private Resource<R> resource; private Engine.LoadStatus loadStatus; private long startTime; private Status status; private Drawable errorDrawable; private Drawable placeholderDrawable; private Drawable fallbackDrawable; private int width; private int height; public static <R> SingleRequest<R> obtain( GlideContext glideContext, Object model, Class<R> transcodeClass, RequestOptions requestOptions, int overrideWidth, int overrideHeight, Priority priority, Target<R> target, RequestListener<R> requestListener, RequestCoordinator requestCoordinator, Engine engine, TransitionFactory<? super R> animationFactory) { @SuppressWarnings("unchecked") SingleRequest<R> request = (SingleRequest<R>) POOL.acquire(); if (request == null) { request = new SingleRequest<>(); } request.init( glideContext, model, transcodeClass, requestOptions, overrideWidth, overrideHeight, priority, target, requestListener, requestCoordinator, engine, animationFactory); return request; } @Synthetic SingleRequest() { // just create, instances are reused with recycle/init } private void init( GlideContext glideContext, Object model, Class<R> transcodeClass, RequestOptions requestOptions, int overrideWidth, int overrideHeight, Priority priority, Target<R> target, RequestListener<R> requestListener, RequestCoordinator requestCoordinator, Engine engine, TransitionFactory<? super R> animationFactory) { this.glideContext = glideContext; this.model = model; this.transcodeClass = transcodeClass; this.requestOptions = requestOptions; this.overrideWidth = overrideWidth; this.overrideHeight = overrideHeight; this.priority = priority; this.target = target; this.requestListener = requestListener; this.requestCoordinator = requestCoordinator; this.engine = engine; this.animationFactory = animationFactory; status = Status.PENDING; } @Override public StateVerifier getVerifier() { return stateVerifier; } @Override public void recycle() { glideContext = null; model = null; transcodeClass = null; requestOptions = null; overrideWidth = -1; overrideHeight = -1; target = null; requestListener = null; requestCoordinator = null; animationFactory = null; loadStatus = null; errorDrawable = null; placeholderDrawable = null; fallbackDrawable = null; width = -1; height = -1; POOL.release(this); } @Override public void begin() { stateVerifier.throwIfRecycled(); startTime = LogTime.getLogTime(); if (model == null) { if (Util.isValidDimensions(overrideWidth, overrideHeight)) { width = overrideWidth; height = overrideHeight; } // Only log at more verbose log levels if the user has set a fallback drawable, because // fallback Drawables indicate the user expects null models occasionally. int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG; onLoadFailed(new GlideException("Received null model"), logLevel); return; } status = Status.WAITING_FOR_SIZE; if (Util.isValidDimensions(overrideWidth, overrideHeight)) { onSizeReady(overrideWidth, overrideHeight); } else { target.getSize(this); } if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE) && canNotifyStatusChanged()) { target.onLoadStarted(getPlaceholderDrawable()); } if (Log.isLoggable(TAG, Log.VERBOSE)) { logV("finished run method in " + LogTime.getElapsedMillis(startTime)); } } /** * Cancels the current load but does not release any resources held by the request and continues * to display the loaded resource if the load completed before the call to cancel. * * <p> Cancelled requests can be restarted with a subsequent call to {@link #begin()}. </p> * * @see #clear() */ void cancel() { stateVerifier.throwIfRecycled(); status = Status.CANCELLED; if (loadStatus != null) { loadStatus.cancel(); loadStatus = null; } } /** * Cancels the current load if it is in progress, clears any resources held onto by the request * and replaces the loaded resource if the load completed with the placeholder. * * <p> Cleared requests can be restarted with a subsequent call to {@link #begin()} </p> * * @see #cancel() */ @Override public void clear() { Util.assertMainThread(); if (status == Status.CLEARED) { return; } cancel(); // Resource must be released before canNotifyStatusChanged is called. if (resource != null) { releaseResource(resource); } if (canNotifyStatusChanged()) { target.onLoadCleared(getPlaceholderDrawable()); } // Must be after cancel(). status = Status.CLEARED; } @Override public boolean isPaused() { return status == Status.PAUSED; } @Override public void pause() { clear(); status = Status.PAUSED; } private void releaseResource(Resource<?> resource) { engine.release(resource); this.resource = null; } @Override public boolean isRunning() { return status == Status.RUNNING || status == Status.WAITING_FOR_SIZE; } @Override public boolean isComplete() { return status == Status.COMPLETE; } @Override public boolean isResourceSet() { return isComplete(); } @Override public boolean isCancelled() { return status == Status.CANCELLED || status == Status.CLEARED; } @Override public boolean isFailed() { return status == Status.FAILED; } private Drawable getErrorDrawable() { if (errorDrawable == null) { errorDrawable = requestOptions.getErrorPlaceholder(); if (errorDrawable == null && requestOptions.getErrorId() > 0) { errorDrawable = loadDrawable(requestOptions.getErrorId()); } } return errorDrawable; } private Drawable getPlaceholderDrawable() { if (placeholderDrawable == null) { placeholderDrawable = requestOptions.getPlaceholderDrawable(); if (placeholderDrawable == null && requestOptions.getPlaceholderId() > 0) { placeholderDrawable = loadDrawable(requestOptions.getPlaceholderId()); } } return placeholderDrawable; } private Drawable getFallbackDrawable() { if (fallbackDrawable == null) { fallbackDrawable = requestOptions.getFallbackDrawable(); if (fallbackDrawable == null && requestOptions.getFallbackId() > 0) { fallbackDrawable = loadDrawable(requestOptions.getFallbackId()); } } return fallbackDrawable; } private Drawable loadDrawable(int resourceId) { Resources resources = glideContext.getResources(); return ResourcesCompat.getDrawable(resources, resourceId, requestOptions.getTheme()); } private void setErrorPlaceholder() { if (!canNotifyStatusChanged()) { return; } Drawable error = model == null ? getFallbackDrawable() : getErrorDrawable(); if (error == null) { error = getPlaceholderDrawable(); } target.onLoadFailed(error); } /** * A callback method that should never be invoked directly. */ @Override public void onSizeReady(int width, int height) { stateVerifier.throwIfRecycled(); if (Log.isLoggable(TAG, Log.VERBOSE)) { logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime)); } if (status != Status.WAITING_FOR_SIZE) { return; } status = Status.RUNNING; float sizeMultiplier = requestOptions.getSizeMultiplier(); this.width = maybeApplySizeMultiplier(width, sizeMultiplier); this.height = maybeApplySizeMultiplier(height, sizeMultiplier); if (Log.isLoggable(TAG, Log.VERBOSE)) { logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime)); } loadStatus = engine.load( glideContext, model, requestOptions.getSignature(), this.width, this.height, requestOptions.getResourceClass(), transcodeClass, priority, requestOptions.getDiskCacheStrategy(), requestOptions.getTransformations(), requestOptions.isTransformationRequired(), requestOptions.getOptions(), requestOptions.isMemoryCacheable(), requestOptions.getUseUnlimitedSourceGeneratorsPool(), requestOptions.getOnlyRetrieveFromCache(), this); if (Log.isLoggable(TAG, Log.VERBOSE)) { logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime)); } } private static int maybeApplySizeMultiplier(int size, float sizeMultiplier) { return size == Target.SIZE_ORIGINAL ? size : Math.round(sizeMultiplier * size); } private boolean canSetResource() { return requestCoordinator == null || requestCoordinator.canSetImage(this); } private boolean canNotifyStatusChanged() { return requestCoordinator == null || requestCoordinator.canNotifyStatusChanged(this); } private boolean isFirstReadyResource() { return requestCoordinator == null || !requestCoordinator.isAnyResourceSet(); } private void notifyLoadSuccess() { if (requestCoordinator != null) { requestCoordinator.onRequestSuccess(this); } } /** * A callback method that should never be invoked directly. */ @SuppressWarnings("unchecked") @Override public void onResourceReady(Resource<?> resource, DataSource dataSource) { stateVerifier.throwIfRecycled(); loadStatus = null; if (resource == null) { GlideException exception = new GlideException("Expected to receive a Resource<R> with an " + "object of " + transcodeClass + " inside, but instead got null."); onLoadFailed(exception); return; } Object received = resource.get(); if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) { releaseResource(resource); GlideException exception = new GlideException("Expected to receive an object of " + transcodeClass + " but instead" + " got " + (received != null ? received.getClass() : "") + "{" + received + "} inside" + " " + "Resource{" + resource + "}." + (received != null ? "" : " " + "To indicate failure return a null Resource " + "object, rather than a Resource object containing null data.")); onLoadFailed(exception); return; } if (!canSetResource()) { releaseResource(resource); // We can't put the status to complete before asking canSetResource(). status = Status.COMPLETE; return; } onResourceReady((Resource<R>) resource, (R) received, dataSource); } /** * Internal {@link #onResourceReady(Resource, DataSource)} where arguments are known to be safe. * * @param resource original {@link Resource}, never <code>null</code> * @param result object returned by {@link Resource#get()}, checked for type and never * <code>null</code> */ private void onResourceReady(Resource<R> resource, R result, DataSource dataSource) { // We must call isFirstReadyResource before setting status. boolean isFirstResource = isFirstReadyResource(); status = Status.COMPLETE; this.resource = resource; if (glideContext.getLogLevel() <= Log.DEBUG) { Log.d(GLIDE_TAG, "Finished loading " + result.getClass().getSimpleName() + " from " + dataSource + " for " + model + " with size [" + width + "x" + height + "] in " + LogTime.getElapsedMillis(startTime) + " ms"); } if (requestListener == null || !requestListener.onResourceReady(result, model, target, dataSource, isFirstResource)) { Transition<? super R> animation = animationFactory.build(dataSource, isFirstResource); target.onResourceReady(result, animation); } notifyLoadSuccess(); } /** * A callback method that should never be invoked directly. */ @Override public void onLoadFailed(GlideException e) { onLoadFailed(e, Log.WARN); } private void onLoadFailed(GlideException e, int maxLogLevel) { stateVerifier.throwIfRecycled(); int logLevel = glideContext.getLogLevel(); if (logLevel <= maxLogLevel) { Log.w(GLIDE_TAG, "Load failed for " + model + " with size [" + width + "x" + height + "]", e); if (logLevel <= Log.INFO) { e.logRootCauses(GLIDE_TAG); } } loadStatus = null; status = Status.FAILED; //TODO: what if this is a thumbnail request? if (requestListener == null || !requestListener.onLoadFailed(e, model, target, isFirstReadyResource())) { setErrorPlaceholder(); } } private void logV(String message) { Log.v(TAG, message + " this: " + tag); } }