Move to Android N-MR1 SDK.
[android_tools.git] / sdk / sources / android-25 / com / android / volley / toolbox / ImageLoader.java
blobd5305e3c9447320a90a401c7054c0a54b025ca3d
1 /**
2 * Copyright (C) 2013 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.android.volley.toolbox;
18 import android.graphics.Bitmap;
19 import android.graphics.Bitmap.Config;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.widget.ImageView;
23 import android.widget.ImageView.ScaleType;
24 import com.android.volley.Request;
25 import com.android.volley.RequestQueue;
26 import com.android.volley.Response.ErrorListener;
27 import com.android.volley.Response.Listener;
28 import com.android.volley.VolleyError;
30 import java.util.HashMap;
31 import java.util.LinkedList;
33 /**
34 * Helper that handles loading and caching images from remote URLs.
36 * The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
37 * and to pass in the default image listener provided by
38 * {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
39 * this class must be made from the main thead, and all responses will be delivered to the main
40 * thread as well.
42 public class ImageLoader {
43 /** RequestQueue for dispatching ImageRequests onto. */
44 private final RequestQueue mRequestQueue;
46 /** Amount of time to wait after first response arrives before delivering all responses. */
47 private int mBatchResponseDelayMs = 100;
49 /** The cache implementation to be used as an L1 cache before calling into volley. */
50 private final ImageCache mCache;
52 /**
53 * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
54 * that we can coalesce multiple requests to the same URL into a single network request.
56 private final HashMap<String, BatchedImageRequest> mInFlightRequests =
57 new HashMap<String, BatchedImageRequest>();
59 /** HashMap of the currently pending responses (waiting to be delivered). */
60 private final HashMap<String, BatchedImageRequest> mBatchedResponses =
61 new HashMap<String, BatchedImageRequest>();
63 /** Handler to the main thread. */
64 private final Handler mHandler = new Handler(Looper.getMainLooper());
66 /** Runnable for in-flight response delivery. */
67 private Runnable mRunnable;
69 /**
70 * Simple cache adapter interface. If provided to the ImageLoader, it
71 * will be used as an L1 cache before dispatch to Volley. Implementations
72 * must not block. Implementation with an LruCache is recommended.
74 public interface ImageCache {
75 public Bitmap getBitmap(String url);
76 public void putBitmap(String url, Bitmap bitmap);
79 /**
80 * Constructs a new ImageLoader.
81 * @param queue The RequestQueue to use for making image requests.
82 * @param imageCache The cache to use as an L1 cache.
84 public ImageLoader(RequestQueue queue, ImageCache imageCache) {
85 mRequestQueue = queue;
86 mCache = imageCache;
89 /**
90 * The default implementation of ImageListener which handles basic functionality
91 * of showing a default image until the network response is received, at which point
92 * it will switch to either the actual image or the error image.
93 * @param view The imageView that the listener is associated with.
94 * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
95 * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
97 public static ImageListener getImageListener(final ImageView view,
98 final int defaultImageResId, final int errorImageResId) {
99 return new ImageListener() {
100 @Override
101 public void onErrorResponse(VolleyError error) {
102 if (errorImageResId != 0) {
103 view.setImageResource(errorImageResId);
107 @Override
108 public void onResponse(ImageContainer response, boolean isImmediate) {
109 if (response.getBitmap() != null) {
110 view.setImageBitmap(response.getBitmap());
111 } else if (defaultImageResId != 0) {
112 view.setImageResource(defaultImageResId);
119 * Interface for the response handlers on image requests.
121 * The call flow is this:
122 * 1. Upon being attached to a request, onResponse(response, true) will
123 * be invoked to reflect any cached data that was already available. If the
124 * data was available, response.getBitmap() will be non-null.
126 * 2. After a network response returns, only one of the following cases will happen:
127 * - onResponse(response, false) will be called if the image was loaded.
128 * or
129 * - onErrorResponse will be called if there was an error loading the image.
131 public interface ImageListener extends ErrorListener {
133 * Listens for non-error changes to the loading of the image request.
135 * @param response Holds all information pertaining to the request, as well
136 * as the bitmap (if it is loaded).
137 * @param isImmediate True if this was called during ImageLoader.get() variants.
138 * This can be used to differentiate between a cached image loading and a network
139 * image loading in order to, for example, run an animation to fade in network loaded
140 * images.
142 public void onResponse(ImageContainer response, boolean isImmediate);
146 * Checks if the item is available in the cache.
147 * @param requestUrl The url of the remote image
148 * @param maxWidth The maximum width of the returned image.
149 * @param maxHeight The maximum height of the returned image.
150 * @return True if the item exists in cache, false otherwise.
152 public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
153 return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
157 * Checks if the item is available in the cache.
159 * @param requestUrl The url of the remote image
160 * @param maxWidth The maximum width of the returned image.
161 * @param maxHeight The maximum height of the returned image.
162 * @param scaleType The scaleType of the imageView.
163 * @return True if the item exists in cache, false otherwise.
165 public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
166 throwIfNotOnMainThread();
168 String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
169 return mCache.getBitmap(cacheKey) != null;
173 * Returns an ImageContainer for the requested URL.
175 * The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
176 * If the default was returned, the {@link ImageLoader} will be invoked when the
177 * request is fulfilled.
179 * @param requestUrl The URL of the image to be loaded.
181 public ImageContainer get(String requestUrl, final ImageListener listener) {
182 return get(requestUrl, listener, 0, 0);
186 * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with
187 * {@code Scaletype == ScaleType.CENTER_INSIDE}.
189 public ImageContainer get(String requestUrl, ImageListener imageListener,
190 int maxWidth, int maxHeight) {
191 return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
195 * Issues a bitmap request with the given URL if that image is not available
196 * in the cache, and returns a bitmap container that contains all of the data
197 * relating to the request (as well as the default image if the requested
198 * image is not available).
199 * @param requestUrl The url of the remote image
200 * @param imageListener The listener to call when the remote image is loaded
201 * @param maxWidth The maximum width of the returned image.
202 * @param maxHeight The maximum height of the returned image.
203 * @param scaleType The ImageViews ScaleType used to calculate the needed image size.
204 * @return A container object that contains all of the properties of the request, as well as
205 * the currently available image (default if remote is not loaded).
207 public ImageContainer get(String requestUrl, ImageListener imageListener,
208 int maxWidth, int maxHeight, ScaleType scaleType) {
210 // only fulfill requests that were initiated from the main thread.
211 throwIfNotOnMainThread();
213 final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
215 // Try to look up the request in the cache of remote images.
216 Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
217 if (cachedBitmap != null) {
218 // Return the cached bitmap.
219 ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
220 imageListener.onResponse(container, true);
221 return container;
224 // The bitmap did not exist in the cache, fetch it!
225 ImageContainer imageContainer =
226 new ImageContainer(null, requestUrl, cacheKey, imageListener);
228 // Update the caller to let them know that they should use the default bitmap.
229 imageListener.onResponse(imageContainer, true);
231 // Check to see if a request is already in-flight.
232 BatchedImageRequest request = mInFlightRequests.get(cacheKey);
233 if (request != null) {
234 // If it is, add this request to the list of listeners.
235 request.addContainer(imageContainer);
236 return imageContainer;
239 // The request is not already in flight. Send the new request to the network and
240 // track it.
241 Request<Bitmap> newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType,
242 cacheKey);
244 mRequestQueue.add(newRequest);
245 mInFlightRequests.put(cacheKey,
246 new BatchedImageRequest(newRequest, imageContainer));
247 return imageContainer;
250 protected Request<Bitmap> makeImageRequest(String requestUrl, int maxWidth, int maxHeight,
251 ScaleType scaleType, final String cacheKey) {
252 return new ImageRequest(requestUrl, new Listener<Bitmap>() {
253 @Override
254 public void onResponse(Bitmap response) {
255 onGetImageSuccess(cacheKey, response);
257 }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
258 @Override
259 public void onErrorResponse(VolleyError error) {
260 onGetImageError(cacheKey, error);
266 * Sets the amount of time to wait after the first response arrives before delivering all
267 * responses. Batching can be disabled entirely by passing in 0.
268 * @param newBatchedResponseDelayMs The time in milliseconds to wait.
270 public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
271 mBatchResponseDelayMs = newBatchedResponseDelayMs;
275 * Handler for when an image was successfully loaded.
276 * @param cacheKey The cache key that is associated with the image request.
277 * @param response The bitmap that was returned from the network.
279 protected void onGetImageSuccess(String cacheKey, Bitmap response) {
280 // cache the image that was fetched.
281 mCache.putBitmap(cacheKey, response);
283 // remove the request from the list of in-flight requests.
284 BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
286 if (request != null) {
287 // Update the response bitmap.
288 request.mResponseBitmap = response;
290 // Send the batched response
291 batchResponse(cacheKey, request);
296 * Handler for when an image failed to load.
297 * @param cacheKey The cache key that is associated with the image request.
299 protected void onGetImageError(String cacheKey, VolleyError error) {
300 // Notify the requesters that something failed via a null result.
301 // Remove this request from the list of in-flight requests.
302 BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
304 if (request != null) {
305 // Set the error for this request
306 request.setError(error);
308 // Send the batched response
309 batchResponse(cacheKey, request);
314 * Container object for all of the data surrounding an image request.
316 public class ImageContainer {
318 * The most relevant bitmap for the container. If the image was in cache, the
319 * Holder to use for the final bitmap (the one that pairs to the requested URL).
321 private Bitmap mBitmap;
323 private final ImageListener mListener;
325 /** The cache key that was associated with the request */
326 private final String mCacheKey;
328 /** The request URL that was specified */
329 private final String mRequestUrl;
332 * Constructs a BitmapContainer object.
333 * @param bitmap The final bitmap (if it exists).
334 * @param requestUrl The requested URL for this container.
335 * @param cacheKey The cache key that identifies the requested URL for this container.
337 public ImageContainer(Bitmap bitmap, String requestUrl,
338 String cacheKey, ImageListener listener) {
339 mBitmap = bitmap;
340 mRequestUrl = requestUrl;
341 mCacheKey = cacheKey;
342 mListener = listener;
346 * Releases interest in the in-flight request (and cancels it if no one else is listening).
348 public void cancelRequest() {
349 if (mListener == null) {
350 return;
353 BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
354 if (request != null) {
355 boolean canceled = request.removeContainerAndCancelIfNecessary(this);
356 if (canceled) {
357 mInFlightRequests.remove(mCacheKey);
359 } else {
360 // check to see if it is already batched for delivery.
361 request = mBatchedResponses.get(mCacheKey);
362 if (request != null) {
363 request.removeContainerAndCancelIfNecessary(this);
364 if (request.mContainers.size() == 0) {
365 mBatchedResponses.remove(mCacheKey);
372 * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
374 public Bitmap getBitmap() {
375 return mBitmap;
379 * Returns the requested URL for this container.
381 public String getRequestUrl() {
382 return mRequestUrl;
387 * Wrapper class used to map a Request to the set of active ImageContainer objects that are
388 * interested in its results.
390 private class BatchedImageRequest {
391 /** The request being tracked */
392 private final Request<?> mRequest;
394 /** The result of the request being tracked by this item */
395 private Bitmap mResponseBitmap;
397 /** Error if one occurred for this response */
398 private VolleyError mError;
400 /** List of all of the active ImageContainers that are interested in the request */
401 private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
404 * Constructs a new BatchedImageRequest object
405 * @param request The request being tracked
406 * @param container The ImageContainer of the person who initiated the request.
408 public BatchedImageRequest(Request<?> request, ImageContainer container) {
409 mRequest = request;
410 mContainers.add(container);
414 * Set the error for this response
416 public void setError(VolleyError error) {
417 mError = error;
421 * Get the error for this response
423 public VolleyError getError() {
424 return mError;
428 * Adds another ImageContainer to the list of those interested in the results of
429 * the request.
431 public void addContainer(ImageContainer container) {
432 mContainers.add(container);
436 * Detatches the bitmap container from the request and cancels the request if no one is
437 * left listening.
438 * @param container The container to remove from the list
439 * @return True if the request was canceled, false otherwise.
441 public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
442 mContainers.remove(container);
443 if (mContainers.size() == 0) {
444 mRequest.cancel();
445 return true;
447 return false;
452 * Starts the runnable for batched delivery of responses if it is not already started.
453 * @param cacheKey The cacheKey of the response being delivered.
454 * @param request The BatchedImageRequest to be delivered.
456 private void batchResponse(String cacheKey, BatchedImageRequest request) {
457 mBatchedResponses.put(cacheKey, request);
458 // If we don't already have a batch delivery runnable in flight, make a new one.
459 // Note that this will be used to deliver responses to all callers in mBatchedResponses.
460 if (mRunnable == null) {
461 mRunnable = new Runnable() {
462 @Override
463 public void run() {
464 for (BatchedImageRequest bir : mBatchedResponses.values()) {
465 for (ImageContainer container : bir.mContainers) {
466 // If one of the callers in the batched request canceled the request
467 // after the response was received but before it was delivered,
468 // skip them.
469 if (container.mListener == null) {
470 continue;
472 if (bir.getError() == null) {
473 container.mBitmap = bir.mResponseBitmap;
474 container.mListener.onResponse(container, false);
475 } else {
476 container.mListener.onErrorResponse(bir.getError());
480 mBatchedResponses.clear();
481 mRunnable = null;
485 // Post the runnable.
486 mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
490 private void throwIfNotOnMainThread() {
491 if (Looper.myLooper() != Looper.getMainLooper()) {
492 throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
496 * Creates a cache key for use with the L1 cache.
497 * @param url The URL of the request.
498 * @param maxWidth The max-width of the output.
499 * @param maxHeight The max-height of the output.
500 * @param scaleType The scaleType of the imageView.
502 private static String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
503 return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
504 .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
505 .toString();