| package com.bumptech.glide.request; |
| |
| import android.content.Context; |
| import android.graphics.drawable.Drawable; |
| import android.util.Log; |
| |
| import com.bumptech.glide.Priority; |
| import com.bumptech.glide.load.Key; |
| import com.bumptech.glide.load.Transformation; |
| import com.bumptech.glide.load.data.DataFetcher; |
| import com.bumptech.glide.load.engine.DiskCacheStrategy; |
| import com.bumptech.glide.load.engine.Engine; |
| import com.bumptech.glide.load.engine.Resource; |
| import com.bumptech.glide.load.model.ModelLoader; |
| import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; |
| import com.bumptech.glide.provider.LoadProvider; |
| import com.bumptech.glide.request.animation.GlideAnimation; |
| import com.bumptech.glide.request.animation.GlideAnimationFactory; |
| import com.bumptech.glide.request.target.SizeReadyCallback; |
| import com.bumptech.glide.request.target.Target; |
| import com.bumptech.glide.util.LogTime; |
| import com.bumptech.glide.util.Util; |
| |
| import java.util.Queue; |
| |
| /** |
| * A {@link Request} that loads a {@link com.bumptech.glide.load.engine.Resource} into a given {@link Target}. |
| * |
| * @param <A> The type of the model that the resource will be loaded from. |
| * @param <T> The type of the data that the resource will be loaded from. |
| * @param <Z> The type of the resource that will be loaded. |
| * @param <R> The type of the resource that will be transcoded from the loaded resource. |
| */ |
| public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback, |
| ResourceCallback { |
| private static final String TAG = "GenericRequest"; |
| private static final Queue<GenericRequest<?, ?, ?, ?>> REQUEST_POOL = Util.createQueue(0); |
| private static final double TO_MEGABYTE = 1d / (1024d * 1024d); |
| |
| 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. */ |
| FAILED, |
| /** Cancelled by the user, may not be restarted. */ |
| CANCELLED, |
| /** Temporarily paused by the system, may be restarted. */ |
| PAUSED, |
| } |
| |
| private final String tag = String.valueOf(hashCode()); |
| |
| private Key signature; |
| private int placeholderResourceId; |
| private int errorResourceId; |
| private Context context; |
| private Transformation<Z> transformation; |
| private LoadProvider<A, T, Z, R> loadProvider; |
| private RequestCoordinator requestCoordinator; |
| private A model; |
| private Class<R> transcodeClass; |
| private boolean isMemoryCacheable; |
| private Priority priority; |
| private Target<R> target; |
| private RequestListener<? super A, R> requestListener; |
| private float sizeMultiplier; |
| private Engine engine; |
| private GlideAnimationFactory<R> animationFactory; |
| private int overrideWidth; |
| private int overrideHeight; |
| private DiskCacheStrategy diskCacheStrategy; |
| |
| private Drawable placeholderDrawable; |
| private Drawable errorDrawable; |
| private boolean loadedFromMemoryCache; |
| // doing our own type check |
| private Resource<?> resource; |
| private Engine.LoadStatus loadStatus; |
| private long startTime; |
| private Status status; |
| |
| public static <A, T, Z, R> GenericRequest<A, T, Z, R> obtain( |
| LoadProvider<A, T, Z, R> loadProvider, |
| A model, |
| Key signature, |
| Context context, |
| Priority priority, |
| Target<R> target, |
| float sizeMultiplier, |
| Drawable placeholderDrawable, |
| int placeholderResourceId, |
| Drawable errorDrawable, |
| int errorResourceId, |
| RequestListener<? super A, R> requestListener, |
| RequestCoordinator requestCoordinator, |
| Engine engine, |
| Transformation<Z> transformation, |
| Class<R> transcodeClass, |
| boolean isMemoryCacheable, |
| GlideAnimationFactory<R> animationFactory, |
| int overrideWidth, |
| int overrideHeight, |
| DiskCacheStrategy diskCacheStrategy) { |
| @SuppressWarnings("unchecked") |
| GenericRequest<A, T, Z, R> request = (GenericRequest<A, T, Z, R>) REQUEST_POOL.poll(); |
| if (request == null) { |
| request = new GenericRequest<A, T, Z, R>(); |
| } |
| request.init(loadProvider, |
| model, |
| signature, |
| context, |
| priority, |
| target, |
| sizeMultiplier, |
| placeholderDrawable, |
| placeholderResourceId, |
| errorDrawable, |
| errorResourceId, |
| requestListener, |
| requestCoordinator, |
| engine, |
| transformation, |
| transcodeClass, |
| isMemoryCacheable, |
| animationFactory, |
| overrideWidth, |
| overrideHeight, |
| diskCacheStrategy); |
| return request; |
| } |
| |
| private GenericRequest() { |
| // just create, instances are reused with recycle/init |
| } |
| |
| @Override |
| public void recycle() { |
| loadProvider = null; |
| model = null; |
| context = null; |
| target = null; |
| placeholderDrawable = null; |
| errorDrawable = null; |
| requestListener = null; |
| requestCoordinator = null; |
| transformation = null; |
| animationFactory = null; |
| loadedFromMemoryCache = false; |
| loadStatus = null; |
| REQUEST_POOL.offer(this); |
| } |
| |
| private void init( |
| LoadProvider<A, T, Z, R> loadProvider, |
| A model, |
| Key signature, |
| Context context, |
| Priority priority, |
| Target<R> target, |
| float sizeMultiplier, |
| Drawable placeholderDrawable, |
| int placeholderResourceId, |
| Drawable errorDrawable, |
| int errorResourceId, |
| RequestListener<? super A, R> requestListener, |
| RequestCoordinator requestCoordinator, |
| Engine engine, |
| Transformation<Z> transformation, |
| Class<R> transcodeClass, |
| boolean isMemoryCacheable, |
| GlideAnimationFactory<R> animationFactory, |
| int overrideWidth, |
| int overrideHeight, |
| DiskCacheStrategy diskCacheStrategy) { |
| this.loadProvider = loadProvider; |
| this.model = model; |
| this.signature = signature; |
| this.context = context.getApplicationContext(); |
| this.priority = priority; |
| this.target = target; |
| this.sizeMultiplier = sizeMultiplier; |
| this.placeholderDrawable = placeholderDrawable; |
| this.placeholderResourceId = placeholderResourceId; |
| this.errorDrawable = errorDrawable; |
| this.errorResourceId = errorResourceId; |
| this.requestListener = requestListener; |
| this.requestCoordinator = requestCoordinator; |
| this.engine = engine; |
| this.transformation = transformation; |
| this.transcodeClass = transcodeClass; |
| this.isMemoryCacheable = isMemoryCacheable; |
| this.animationFactory = animationFactory; |
| this.overrideWidth = overrideWidth; |
| this.overrideHeight = overrideHeight; |
| this.diskCacheStrategy = diskCacheStrategy; |
| status = Status.PENDING; |
| |
| // We allow null models by just setting an error drawable. Null models will always have empty providers, we |
| // simply skip our sanity checks in that unusual case. |
| if (model != null) { |
| check("ModelLoader", loadProvider.getModelLoader(), "try .using(ModelLoader)"); |
| check("Transcoder", loadProvider.getTranscoder(), "try .as*(Class).transcode(ResourceTranscoder)"); |
| check("Transformation", transformation, "try .transform(UnitTransformation.get())"); |
| if (diskCacheStrategy.cacheSource()) { |
| check("SourceEncoder", loadProvider.getSourceEncoder(), |
| "try .sourceEncoder(Encoder) or .diskCacheStrategy(NONE/RESULT)"); |
| } else { |
| check("SourceDecoder", loadProvider.getSourceDecoder(), |
| "try .decoder/.imageDecoder/.videoDecoder(ResourceDecoder) or .diskCacheStrategy(ALL/SOURCE)"); |
| } |
| if (diskCacheStrategy.cacheSource() || diskCacheStrategy.cacheResult()) { |
| // TODO if(resourceClass.isAssignableFrom(InputStream.class) it is possible to wrap sourceDecoder |
| // and use it instead of cacheDecoder: new FileToStreamDecoder<Z>(sourceDecoder) |
| // in that case this shouldn't throw |
| check("CacheDecoder", loadProvider.getCacheDecoder(), |
| "try .cacheDecoder(ResouceDecoder) or .diskCacheStrategy(NONE)"); |
| } |
| if (diskCacheStrategy.cacheResult()) { |
| check("Encoder", loadProvider.getEncoder(), |
| "try .encode(ResourceEncoder) or .diskCacheStrategy(NONE/SOURCE)"); |
| } |
| } |
| } |
| |
| private static void check(String name, Object object, String suggestion) { |
| if (object == null) { |
| StringBuilder message = new StringBuilder(name); |
| message.append(" must not be null"); |
| if (suggestion != null) { |
| message.append(", "); |
| message.append(suggestion); |
| } |
| throw new NullPointerException(message.toString()); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void begin() { |
| startTime = LogTime.getLogTime(); |
| if (model == null) { |
| onException(null); |
| return; |
| } |
| |
| status = Status.WAITING_FOR_SIZE; |
| if (overrideWidth > 0 && overrideHeight > 0) { |
| onSizeReady(overrideWidth, overrideHeight); |
| } else { |
| target.getSize(this); |
| } |
| |
| if (!isComplete() && !isFailed() && 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() { |
| 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(); |
| cancel(); |
| // Resource must be released before canNotifyStatusChanged is called. |
| if (resource != null) { |
| releaseResource(resource); |
| } |
| if (canNotifyStatusChanged()) { |
| target.onLoadCleared(getPlaceholderDrawable()); |
| } |
| } |
| |
| @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; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isRunning() { |
| return status == Status.RUNNING || status == Status.WAITING_FOR_SIZE; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isComplete() { |
| return status == Status.COMPLETE; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isResourceSet() { |
| return isComplete(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isCancelled() { |
| return status == Status.CANCELLED; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isFailed() { |
| return status == Status.FAILED; |
| } |
| |
| private void setErrorPlaceholder(Exception e) { |
| if (!canNotifyStatusChanged()) { |
| return; |
| } |
| |
| Drawable error = getErrorDrawable(); |
| if (error == null) { |
| error = getPlaceholderDrawable(); |
| } |
| target.onLoadFailed(e, error); |
| } |
| |
| private Drawable getErrorDrawable() { |
| if (errorDrawable == null && errorResourceId > 0) { |
| errorDrawable = context.getResources().getDrawable(errorResourceId); |
| } |
| return errorDrawable; |
| } |
| |
| private Drawable getPlaceholderDrawable() { |
| if (placeholderDrawable == null && placeholderResourceId > 0) { |
| placeholderDrawable = context.getResources().getDrawable(placeholderResourceId); |
| } |
| return placeholderDrawable; |
| } |
| |
| /** |
| * A callback method that should never be invoked directly. |
| */ |
| @Override |
| public void onSizeReady(int width, int height) { |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime)); |
| } |
| if (status != Status.WAITING_FOR_SIZE) { |
| return; |
| } |
| status = Status.RUNNING; |
| |
| width = Math.round(sizeMultiplier * width); |
| height = Math.round(sizeMultiplier * height); |
| |
| ModelLoader<A, T> modelLoader = loadProvider.getModelLoader(); |
| final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height); |
| |
| if (dataFetcher == null) { |
| onException(new Exception("Got null fetcher from model loader")); |
| return; |
| } |
| ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder(); |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime)); |
| } |
| loadedFromMemoryCache = true; |
| loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder, |
| priority, isMemoryCacheable, diskCacheStrategy, this); |
| loadedFromMemoryCache = resource != null; |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime)); |
| } |
| } |
| |
| 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(); |
| } |
| |
| /** |
| * A callback method that should never be invoked directly. |
| */ |
| @SuppressWarnings("unchecked") |
| @Override |
| public void onResourceReady(Resource<?> resource) { |
| if (resource == null) { |
| onException(new Exception("Expected to receive a Resource<R> with an object of " + transcodeClass |
| + " inside, but instead got null.")); |
| return; |
| } |
| |
| Object received = resource.get(); |
| if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) { |
| releaseResource(resource); |
| onException(new Exception("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.") |
| )); |
| return; |
| } |
| |
| if (!canSetResource()) { |
| releaseResource(resource); |
| // We can't set the status to complete before asking canSetResource(). |
| status = Status.COMPLETE; |
| return; |
| } |
| |
| onResourceReady(resource, (R) received); |
| } |
| |
| /** |
| * Internal {@link #onResourceReady(Resource)} 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<?> resource, R result) { |
| if (requestListener == null || !requestListener.onResourceReady(result, model, target, loadedFromMemoryCache, |
| isFirstReadyResource())) { |
| GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstReadyResource()); |
| target.onResourceReady(result, animation); |
| } |
| |
| status = Status.COMPLETE; |
| this.resource = resource; |
| |
| if (Log.isLoggable(TAG, Log.VERBOSE)) { |
| logV("Resource ready in " + LogTime.getElapsedMillis(startTime) + " size: " |
| + (resource.getSize() * TO_MEGABYTE) + " fromCache: " + loadedFromMemoryCache); |
| } |
| } |
| |
| /** |
| * A callback method that should never be invoked directly. |
| */ |
| @Override |
| public void onException(Exception e) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "load failed", e); |
| } |
| |
| status = Status.FAILED; |
| //TODO: what if this is a thumbnail request? |
| if (requestListener == null || !requestListener.onException(e, model, target, isFirstReadyResource())) { |
| setErrorPlaceholder(e); |
| } |
| } |
| |
| private void logV(String message) { |
| Log.v(TAG, message + " this: " + tag); |
| } |
| } |