| /* |
| * Copyright (C) 2013 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file |
| * except in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the |
| * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either |
| * express or implied. See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.volley.toolbox; |
| |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap.Config; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.widget.ImageView; |
| import android.widget.ImageView.ScaleType; |
| import androidx.annotation.MainThread; |
| import com.android.volley.Request; |
| import com.android.volley.RequestQueue; |
| import com.android.volley.Response.ErrorListener; |
| import com.android.volley.Response.Listener; |
| import com.android.volley.ResponseDelivery; |
| import com.android.volley.VolleyError; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| |
| /** |
| * Helper that handles loading and caching images from remote URLs. |
| * |
| * <p>The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)} and |
| * to pass in the default image listener provided by {@link ImageLoader#getImageListener(ImageView, |
| * int, int)}. Note that all function calls to this class must be made from the main thread, and all |
| * responses will be delivered to the main thread as well. Custom {@link ResponseDelivery}s which |
| * don't use the main thread are not supported. |
| */ |
| public class ImageLoader { |
| /** RequestQueue for dispatching ImageRequests onto. */ |
| private final RequestQueue mRequestQueue; |
| |
| /** Amount of time to wait after first response arrives before delivering all responses. */ |
| private int mBatchResponseDelayMs = 100; |
| |
| /** The cache implementation to be used as an L1 cache before calling into volley. */ |
| private final ImageCache mCache; |
| |
| /** |
| * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so that we can |
| * coalesce multiple requests to the same URL into a single network request. |
| */ |
| private final HashMap<String, BatchedImageRequest> mInFlightRequests = new HashMap<>(); |
| |
| /** HashMap of the currently pending responses (waiting to be delivered). */ |
| private final HashMap<String, BatchedImageRequest> mBatchedResponses = new HashMap<>(); |
| |
| /** Handler to the main thread. */ |
| private final Handler mHandler = new Handler(Looper.getMainLooper()); |
| |
| /** Runnable for in-flight response delivery. */ |
| private Runnable mRunnable; |
| |
| /** |
| * Simple cache adapter interface. If provided to the ImageLoader, it will be used as an L1 |
| * cache before dispatch to Volley. Implementations must not block. Implementation with an |
| * LruCache is recommended. |
| */ |
| public interface ImageCache { |
| Bitmap getBitmap(String url); |
| |
| void putBitmap(String url, Bitmap bitmap); |
| } |
| |
| /** |
| * Constructs a new ImageLoader. |
| * |
| * @param queue The RequestQueue to use for making image requests. |
| * @param imageCache The cache to use as an L1 cache. |
| */ |
| public ImageLoader(RequestQueue queue, ImageCache imageCache) { |
| mRequestQueue = queue; |
| mCache = imageCache; |
| } |
| |
| /** |
| * The default implementation of ImageListener which handles basic functionality of showing a |
| * default image until the network response is received, at which point it will switch to either |
| * the actual image or the error image. |
| * |
| * @param view The imageView that the listener is associated with. |
| * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist. |
| * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist. |
| */ |
| public static ImageListener getImageListener( |
| final ImageView view, final int defaultImageResId, final int errorImageResId) { |
| return new ImageListener() { |
| @Override |
| public void onErrorResponse(VolleyError error) { |
| if (errorImageResId != 0) { |
| view.setImageResource(errorImageResId); |
| } |
| } |
| |
| @Override |
| public void onResponse(ImageContainer response, boolean isImmediate) { |
| if (response.getBitmap() != null) { |
| view.setImageBitmap(response.getBitmap()); |
| } else if (defaultImageResId != 0) { |
| view.setImageResource(defaultImageResId); |
| } |
| } |
| }; |
| } |
| |
| /** |
| * Interface for the response handlers on image requests. |
| * |
| * <p>The call flow is this: 1. Upon being attached to a request, onResponse(response, true) |
| * will be invoked to reflect any cached data that was already available. If the data was |
| * available, response.getBitmap() will be non-null. |
| * |
| * <p>2. After a network response returns, only one of the following cases will happen: - |
| * onResponse(response, false) will be called if the image was loaded. or - onErrorResponse will |
| * be called if there was an error loading the image. |
| */ |
| public interface ImageListener extends ErrorListener { |
| /** |
| * Listens for non-error changes to the loading of the image request. |
| * |
| * @param response Holds all information pertaining to the request, as well as the bitmap |
| * (if it is loaded). |
| * @param isImmediate True if this was called during ImageLoader.get() variants. This can be |
| * used to differentiate between a cached image loading and a network image loading in |
| * order to, for example, run an animation to fade in network loaded images. |
| */ |
| void onResponse(ImageContainer response, boolean isImmediate); |
| } |
| |
| /** |
| * Checks if the item is available in the cache. |
| * |
| * @param requestUrl The url of the remote image |
| * @param maxWidth The maximum width of the returned image. |
| * @param maxHeight The maximum height of the returned image. |
| * @return True if the item exists in cache, false otherwise. |
| */ |
| public boolean isCached(String requestUrl, int maxWidth, int maxHeight) { |
| return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); |
| } |
| |
| /** |
| * Checks if the item is available in the cache. |
| * |
| * <p>Must be called from the main thread. |
| * |
| * @param requestUrl The url of the remote image |
| * @param maxWidth The maximum width of the returned image. |
| * @param maxHeight The maximum height of the returned image. |
| * @param scaleType The scaleType of the imageView. |
| * @return True if the item exists in cache, false otherwise. |
| */ |
| @MainThread |
| public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) { |
| Threads.throwIfNotOnMainThread(); |
| |
| String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); |
| return mCache.getBitmap(cacheKey) != null; |
| } |
| |
| /** |
| * Returns an ImageContainer for the requested URL. |
| * |
| * <p>The ImageContainer will contain either the specified default bitmap or the loaded bitmap. |
| * If the default was returned, the {@link ImageLoader} will be invoked when the request is |
| * fulfilled. |
| * |
| * @param requestUrl The URL of the image to be loaded. |
| */ |
| public ImageContainer get(String requestUrl, final ImageListener listener) { |
| return get(requestUrl, listener, /* maxWidth= */ 0, /* maxHeight= */ 0); |
| } |
| |
| /** |
| * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with {@code |
| * Scaletype == ScaleType.CENTER_INSIDE}. |
| */ |
| public ImageContainer get( |
| String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { |
| return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); |
| } |
| |
| /** |
| * Issues a bitmap request with the given URL if that image is not available in the cache, and |
| * returns a bitmap container that contains all of the data relating to the request (as well as |
| * the default image if the requested image is not available). |
| * |
| * <p>Must be called from the main thread. |
| * |
| * @param requestUrl The url of the remote image |
| * @param imageListener The listener to call when the remote image is loaded |
| * @param maxWidth The maximum width of the returned image. |
| * @param maxHeight The maximum height of the returned image. |
| * @param scaleType The ImageViews ScaleType used to calculate the needed image size. |
| * @return A container object that contains all of the properties of the request, as well as the |
| * currently available image (default if remote is not loaded). |
| */ |
| @MainThread |
| public ImageContainer get( |
| String requestUrl, |
| ImageListener imageListener, |
| int maxWidth, |
| int maxHeight, |
| ScaleType scaleType) { |
| |
| // only fulfill requests that were initiated from the main thread. |
| Threads.throwIfNotOnMainThread(); |
| |
| final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); |
| |
| // Try to look up the request in the cache of remote images. |
| Bitmap cachedBitmap = mCache.getBitmap(cacheKey); |
| if (cachedBitmap != null) { |
| // Return the cached bitmap. |
| ImageContainer container = |
| new ImageContainer( |
| cachedBitmap, requestUrl, /* cacheKey= */ null, /* listener= */ null); |
| imageListener.onResponse(container, true); |
| return container; |
| } |
| |
| // The bitmap did not exist in the cache, fetch it! |
| ImageContainer imageContainer = |
| new ImageContainer(null, requestUrl, cacheKey, imageListener); |
| |
| // Update the caller to let them know that they should use the default bitmap. |
| imageListener.onResponse(imageContainer, true); |
| |
| // Check to see if a request is already in-flight or completed but pending batch delivery. |
| BatchedImageRequest request = mInFlightRequests.get(cacheKey); |
| if (request == null) { |
| request = mBatchedResponses.get(cacheKey); |
| } |
| if (request != null) { |
| // If it is, add this request to the list of listeners. |
| request.addContainer(imageContainer); |
| return imageContainer; |
| } |
| |
| // The request is not already in flight. Send the new request to the network and |
| // track it. |
| Request<Bitmap> newRequest = |
| makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey); |
| |
| mRequestQueue.add(newRequest); |
| mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); |
| return imageContainer; |
| } |
| |
| protected Request<Bitmap> makeImageRequest( |
| String requestUrl, |
| int maxWidth, |
| int maxHeight, |
| ScaleType scaleType, |
| final String cacheKey) { |
| return new ImageRequest( |
| requestUrl, |
| new Listener<Bitmap>() { |
| @Override |
| public void onResponse(Bitmap response) { |
| onGetImageSuccess(cacheKey, response); |
| } |
| }, |
| maxWidth, |
| maxHeight, |
| scaleType, |
| Config.RGB_565, |
| new ErrorListener() { |
| @Override |
| public void onErrorResponse(VolleyError error) { |
| onGetImageError(cacheKey, error); |
| } |
| }); |
| } |
| |
| /** |
| * Sets the amount of time to wait after the first response arrives before delivering all |
| * responses. Batching can be disabled entirely by passing in 0. |
| * |
| * @param newBatchedResponseDelayMs The time in milliseconds to wait. |
| */ |
| public void setBatchedResponseDelay(int newBatchedResponseDelayMs) { |
| mBatchResponseDelayMs = newBatchedResponseDelayMs; |
| } |
| |
| /** |
| * Handler for when an image was successfully loaded. |
| * |
| * @param cacheKey The cache key that is associated with the image request. |
| * @param response The bitmap that was returned from the network. |
| */ |
| protected void onGetImageSuccess(String cacheKey, Bitmap response) { |
| // cache the image that was fetched. |
| mCache.putBitmap(cacheKey, response); |
| |
| // remove the request from the list of in-flight requests. |
| BatchedImageRequest request = mInFlightRequests.remove(cacheKey); |
| |
| if (request != null) { |
| // Update the response bitmap. |
| request.mResponseBitmap = response; |
| |
| // Send the batched response |
| batchResponse(cacheKey, request); |
| } |
| } |
| |
| /** |
| * Handler for when an image failed to load. |
| * |
| * @param cacheKey The cache key that is associated with the image request. |
| */ |
| protected void onGetImageError(String cacheKey, VolleyError error) { |
| // Notify the requesters that something failed via a null result. |
| // Remove this request from the list of in-flight requests. |
| BatchedImageRequest request = mInFlightRequests.remove(cacheKey); |
| |
| if (request != null) { |
| // Set the error for this request |
| request.setError(error); |
| |
| // Send the batched response |
| batchResponse(cacheKey, request); |
| } |
| } |
| |
| /** Container object for all of the data surrounding an image request. */ |
| public class ImageContainer { |
| /** |
| * The most relevant bitmap for the container. If the image was in cache, the Holder to use |
| * for the final bitmap (the one that pairs to the requested URL). |
| */ |
| private Bitmap mBitmap; |
| |
| private final ImageListener mListener; |
| |
| /** The cache key that was associated with the request */ |
| private final String mCacheKey; |
| |
| /** The request URL that was specified */ |
| private final String mRequestUrl; |
| |
| /** |
| * Constructs a BitmapContainer object. |
| * |
| * @param bitmap The final bitmap (if it exists). |
| * @param requestUrl The requested URL for this container. |
| * @param cacheKey The cache key that identifies the requested URL for this container. |
| */ |
| public ImageContainer( |
| Bitmap bitmap, String requestUrl, String cacheKey, ImageListener listener) { |
| mBitmap = bitmap; |
| mRequestUrl = requestUrl; |
| mCacheKey = cacheKey; |
| mListener = listener; |
| } |
| |
| /** |
| * Releases interest in the in-flight request (and cancels it if no one else is listening). |
| * |
| * <p>Must be called from the main thread. |
| */ |
| @MainThread |
| public void cancelRequest() { |
| Threads.throwIfNotOnMainThread(); |
| |
| if (mListener == null) { |
| return; |
| } |
| |
| BatchedImageRequest request = mInFlightRequests.get(mCacheKey); |
| if (request != null) { |
| boolean canceled = request.removeContainerAndCancelIfNecessary(this); |
| if (canceled) { |
| mInFlightRequests.remove(mCacheKey); |
| } |
| } else { |
| // check to see if it is already batched for delivery. |
| request = mBatchedResponses.get(mCacheKey); |
| if (request != null) { |
| request.removeContainerAndCancelIfNecessary(this); |
| if (request.mContainers.size() == 0) { |
| mBatchedResponses.remove(mCacheKey); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the bitmap associated with the request URL if it has been loaded, null otherwise. |
| */ |
| public Bitmap getBitmap() { |
| return mBitmap; |
| } |
| |
| /** Returns the requested URL for this container. */ |
| public String getRequestUrl() { |
| return mRequestUrl; |
| } |
| } |
| |
| /** |
| * Wrapper class used to map a Request to the set of active ImageContainer objects that are |
| * interested in its results. |
| */ |
| private static class BatchedImageRequest { |
| /** The request being tracked */ |
| private final Request<?> mRequest; |
| |
| /** The result of the request being tracked by this item */ |
| private Bitmap mResponseBitmap; |
| |
| /** Error if one occurred for this response */ |
| private VolleyError mError; |
| |
| /** List of all of the active ImageContainers that are interested in the request */ |
| private final List<ImageContainer> mContainers = new ArrayList<>(); |
| |
| /** |
| * Constructs a new BatchedImageRequest object |
| * |
| * @param request The request being tracked |
| * @param container The ImageContainer of the person who initiated the request. |
| */ |
| public BatchedImageRequest(Request<?> request, ImageContainer container) { |
| mRequest = request; |
| mContainers.add(container); |
| } |
| |
| /** Set the error for this response */ |
| public void setError(VolleyError error) { |
| mError = error; |
| } |
| |
| /** Get the error for this response */ |
| public VolleyError getError() { |
| return mError; |
| } |
| |
| /** |
| * Adds another ImageContainer to the list of those interested in the results of the |
| * request. |
| */ |
| public void addContainer(ImageContainer container) { |
| mContainers.add(container); |
| } |
| |
| /** |
| * Detaches the bitmap container from the request and cancels the request if no one is left |
| * listening. |
| * |
| * @param container The container to remove from the list |
| * @return True if the request was canceled, false otherwise. |
| */ |
| public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { |
| mContainers.remove(container); |
| if (mContainers.size() == 0) { |
| mRequest.cancel(); |
| return true; |
| } |
| return false; |
| } |
| } |
| |
| /** |
| * Starts the runnable for batched delivery of responses if it is not already started. |
| * |
| * @param cacheKey The cacheKey of the response being delivered. |
| * @param request The BatchedImageRequest to be delivered. |
| */ |
| private void batchResponse(String cacheKey, BatchedImageRequest request) { |
| mBatchedResponses.put(cacheKey, request); |
| // If we don't already have a batch delivery runnable in flight, make a new one. |
| // Note that this will be used to deliver responses to all callers in mBatchedResponses. |
| if (mRunnable == null) { |
| mRunnable = |
| new Runnable() { |
| @Override |
| public void run() { |
| for (BatchedImageRequest bir : mBatchedResponses.values()) { |
| for (ImageContainer container : bir.mContainers) { |
| // If one of the callers in the batched request canceled the |
| // request |
| // after the response was received but before it was delivered, |
| // skip them. |
| if (container.mListener == null) { |
| continue; |
| } |
| if (bir.getError() == null) { |
| container.mBitmap = bir.mResponseBitmap; |
| container.mListener.onResponse(container, false); |
| } else { |
| container.mListener.onErrorResponse(bir.getError()); |
| } |
| } |
| } |
| mBatchedResponses.clear(); |
| mRunnable = null; |
| } |
| }; |
| // Post the runnable. |
| mHandler.postDelayed(mRunnable, mBatchResponseDelayMs); |
| } |
| } |
| |
| /** |
| * Creates a cache key for use with the L1 cache. |
| * |
| * @param url The URL of the request. |
| * @param maxWidth The max-width of the output. |
| * @param maxHeight The max-height of the output. |
| * @param scaleType The scaleType of the imageView. |
| */ |
| private static String getCacheKey( |
| String url, int maxWidth, int maxHeight, ScaleType scaleType) { |
| return new StringBuilder(url.length() + 12) |
| .append("#W") |
| .append(maxWidth) |
| .append("#H") |
| .append(maxHeight) |
| .append("#S") |
| .append(scaleType.ordinal()) |
| .append(url) |
| .toString(); |
| } |
| } |