blob: fe0f32ad927792d3e8b49087883f58bf9216fad6 [file] [log] [blame]
Aurash Mahbodd62a6162013-05-08 00:26:10 -07001/**
2 * Copyright (C) 2013 The Android Open Source Project
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16package com.android.volley.toolbox;
17
18import android.graphics.Bitmap;
19import android.graphics.Bitmap.Config;
20import android.os.Handler;
21import android.os.Looper;
22import android.widget.ImageView;
23
24import com.android.volley.Request;
25import com.android.volley.RequestQueue;
26import com.android.volley.Response.ErrorListener;
27import com.android.volley.Response.Listener;
28import com.android.volley.VolleyError;
29import com.android.volley.toolbox.ImageRequest;
30
31import java.util.HashMap;
32import java.util.LinkedList;
33
34/**
35 * Helper that handles loading and caching images from remote URLs.
36 *
37 * The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
38 * and to pass in the default image listener provided by
39 * {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
40 * this class must be made from the main thead, and all responses will be delivered to the main
41 * thread as well.
42 */
43public class ImageLoader {
44 /** RequestQueue for dispatching ImageRequests onto. */
45 private final RequestQueue mRequestQueue;
46
47 /** Amount of time to wait after first response arrives before delivering all responses. */
48 private int mBatchResponseDelayMs = 100;
49
50 /** The cache implementation to be used as an L1 cache before calling into volley. */
51 private final ImageCache mCache;
52
53 /**
54 * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
55 * that we can coalesce multiple requests to the same URL into a single network request.
56 */
57 private final HashMap<String, BatchedImageRequest> mInFlightRequests =
58 new HashMap<String, BatchedImageRequest>();
59
60 /** HashMap of the currently pending responses (waiting to be delivered). */
61 private final HashMap<String, BatchedImageRequest> mBatchedResponses =
62 new HashMap<String, BatchedImageRequest>();
63
64 /** Handler to the main thread. */
65 private final Handler mHandler = new Handler(Looper.getMainLooper());
66
67 /** Runnable for in-flight response delivery. */
68 private Runnable mRunnable;
69
70 /**
71 * Simple cache adapter interface. If provided to the ImageLoader, it
72 * will be used as an L1 cache before dispatch to Volley. Implementations
73 * must not block. Implementation with an LruCache is recommended.
74 */
75 public interface ImageCache {
76 public Bitmap getBitmap(String url);
77 public void putBitmap(String url, Bitmap bitmap);
78 }
79
80 /**
81 * Constructs a new ImageLoader.
82 * @param queue The RequestQueue to use for making image requests.
83 * @param imageCache The cache to use as an L1 cache.
84 */
85 public ImageLoader(RequestQueue queue, ImageCache imageCache) {
86 mRequestQueue = queue;
87 mCache = imageCache;
88 }
89
90 /**
91 * The default implementation of ImageListener which handles basic functionality
92 * of showing a default image until the network response is received, at which point
93 * it will switch to either the actual image or the error image.
94 * @param imageView The imageView that the listener is associated with.
95 * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
96 * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
97 */
98 public static ImageListener getImageListener(final ImageView view,
99 final int defaultImageResId, final int errorImageResId) {
100 return new ImageListener() {
101 @Override
102 public void onErrorResponse(VolleyError error) {
103 if (errorImageResId != 0) {
104 view.setImageResource(errorImageResId);
105 }
106 }
107
108 @Override
109 public void onResponse(ImageContainer response, boolean isImmediate) {
110 if (response.getBitmap() != null) {
111 view.setImageBitmap(response.getBitmap());
112 } else if (defaultImageResId != 0) {
113 view.setImageResource(defaultImageResId);
114 }
115 }
116 };
117 }
118
119 /**
120 * Interface for the response handlers on image requests.
121 *
122 * The call flow is this:
123 * 1. Upon being attached to a request, onResponse(response, true) will
124 * be invoked to reflect any cached data that was already available. If the
125 * data was available, response.getBitmap() will be non-null.
126 *
127 * 2. After a network response returns, only one of the following cases will happen:
128 * - onResponse(response, false) will be called if the image was loaded.
129 * or
130 * - onErrorResponse will be called if there was an error loading the image.
131 */
132 public interface ImageListener extends ErrorListener {
133 /**
134 * Listens for non-error changes to the loading of the image request.
135 *
136 * @param response Holds all information pertaining to the request, as well
137 * as the bitmap (if it is loaded).
138 * @param isImmediate True if this was called during ImageLoader.get() variants.
139 * This can be used to differentiate between a cached image loading and a network
140 * image loading in order to, for example, run an animation to fade in network loaded
141 * images.
142 */
143 public void onResponse(ImageContainer response, boolean isImmediate);
144 }
145
146 /**
andaagar285d4722013-05-22 19:28:14 +0200147 * Checks if the item is available in the cache.
148 * @param requestUrl The url of the remote image
149 * @param maxWidth The maximum width of the returned image.
150 * @param maxHeight The maximum height of the returned image.
151 * @return True if the item exists in cache, false otherwise.
152 */
153 public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
154 throwIfNotOnMainThread();
155
156 String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
157 return mCache.getBitmap(cacheKey) != null;
158 }
159
160 /**
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700161 * Returns an ImageContainer for the requested URL.
162 *
163 * The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
164 * If the default was returned, the {@link ImageLoader} will be invoked when the
165 * request is fulfilled.
166 *
167 * @param requestUrl The URL of the image to be loaded.
168 * @param defaultImage Optional default image to return until the actual image is loaded.
169 */
170 public ImageContainer get(String requestUrl, final ImageListener listener) {
171 return get(requestUrl, listener, 0, 0);
172 }
173
174 /**
175 * Issues a bitmap request with the given URL if that image is not available
176 * in the cache, and returns a bitmap container that contains all of the data
177 * relating to the request (as well as the default image if the requested
178 * image is not available).
179 * @param requestUrl The url of the remote image
180 * @param imageListener The listener to call when the remote image is loaded
181 * @param maxWidth The maximum width of the returned image.
182 * @param maxHeight The maximum height of the returned image.
183 * @return A container object that contains all of the properties of the request, as well as
184 * the currently available image (default if remote is not loaded).
185 */
186 public ImageContainer get(String requestUrl, ImageListener imageListener,
187 int maxWidth, int maxHeight) {
188 // only fulfill requests that were initiated from the main thread.
189 throwIfNotOnMainThread();
190
191 final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
192
193 // Try to look up the request in the cache of remote images.
194 Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
195 if (cachedBitmap != null) {
196 // Return the cached bitmap.
197 ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
198 imageListener.onResponse(container, true);
199 return container;
200 }
201
202 // The bitmap did not exist in the cache, fetch it!
203 ImageContainer imageContainer =
204 new ImageContainer(null, requestUrl, cacheKey, imageListener);
205
206 // Update the caller to let them know that they should use the default bitmap.
207 imageListener.onResponse(imageContainer, true);
208
209 // Check to see if a request is already in-flight.
210 BatchedImageRequest request = mInFlightRequests.get(cacheKey);
211 if (request != null) {
212 // If it is, add this request to the list of listeners.
213 request.addContainer(imageContainer);
214 return imageContainer;
215 }
216
217 // The request is not already in flight. Send the new request to the network and
218 // track it.
219 Request<?> newRequest =
220 new ImageRequest(requestUrl, new Listener<Bitmap>() {
221 @Override
222 public void onResponse(Bitmap response) {
223 onGetImageSuccess(cacheKey, response);
224 }
225 }, maxWidth, maxHeight,
226 Config.RGB_565, new ErrorListener() {
227 @Override
228 public void onErrorResponse(VolleyError error) {
229 onGetImageError(cacheKey, error);
230 }
231 });
232
233 mRequestQueue.add(newRequest);
234 mInFlightRequests.put(cacheKey,
235 new BatchedImageRequest(newRequest, imageContainer));
236 return imageContainer;
237 }
238
239 /**
240 * Sets the amount of time to wait after the first response arrives before delivering all
241 * responses. Batching can be disabled entirely by passing in 0.
242 * @param newBatchedResponseDelayMs The time in milliseconds to wait.
243 */
244 public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
245 mBatchResponseDelayMs = newBatchedResponseDelayMs;
246 }
247
248 /**
249 * Handler for when an image was successfully loaded.
250 * @param cacheKey The cache key that is associated with the image request.
251 * @param response The bitmap that was returned from the network.
252 */
253 private void onGetImageSuccess(String cacheKey, Bitmap response) {
254 // cache the image that was fetched.
255 mCache.putBitmap(cacheKey, response);
256
257 // remove the request from the list of in-flight requests.
258 BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
259
260 if (request != null) {
261 // Update the response bitmap.
262 request.mResponseBitmap = response;
263
264 // Send the batched response
Cameron Ketchamdc535522013-05-23 17:40:08 -0400265 batchResponse(cacheKey, request);
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700266 }
267 }
268
269 /**
270 * Handler for when an image failed to load.
271 * @param cacheKey The cache key that is associated with the image request.
272 */
273 private void onGetImageError(String cacheKey, VolleyError error) {
274 // Notify the requesters that something failed via a null result.
275 // Remove this request from the list of in-flight requests.
276 BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
277
278 if (request != null) {
Andrew Sutherlandee9d4502014-03-29 18:09:47 -0500279 // Set the error for this request
280 request.setError(error);
281
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700282 // Send the batched response
Cameron Ketchamdc535522013-05-23 17:40:08 -0400283 batchResponse(cacheKey, request);
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700284 }
285 }
286
287 /**
288 * Container object for all of the data surrounding an image request.
289 */
290 public class ImageContainer {
291 /**
292 * The most relevant bitmap for the container. If the image was in cache, the
293 * Holder to use for the final bitmap (the one that pairs to the requested URL).
294 */
295 private Bitmap mBitmap;
296
297 private final ImageListener mListener;
298
299 /** The cache key that was associated with the request */
300 private final String mCacheKey;
301
302 /** The request URL that was specified */
303 private final String mRequestUrl;
304
305 /**
306 * Constructs a BitmapContainer object.
307 * @param bitmap The final bitmap (if it exists).
308 * @param requestUrl The requested URL for this container.
309 * @param cacheKey The cache key that identifies the requested URL for this container.
310 */
311 public ImageContainer(Bitmap bitmap, String requestUrl,
312 String cacheKey, ImageListener listener) {
313 mBitmap = bitmap;
314 mRequestUrl = requestUrl;
315 mCacheKey = cacheKey;
316 mListener = listener;
317 }
318
319 /**
320 * Releases interest in the in-flight request (and cancels it if no one else is listening).
321 */
322 public void cancelRequest() {
323 if (mListener == null) {
324 return;
325 }
326
327 BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
328 if (request != null) {
329 boolean canceled = request.removeContainerAndCancelIfNecessary(this);
330 if (canceled) {
331 mInFlightRequests.remove(mCacheKey);
332 }
333 } else {
334 // check to see if it is already batched for delivery.
335 request = mBatchedResponses.get(mCacheKey);
336 if (request != null) {
337 request.removeContainerAndCancelIfNecessary(this);
338 if (request.mContainers.size() == 0) {
339 mBatchedResponses.remove(mCacheKey);
340 }
341 }
342 }
343 }
344
345 /**
346 * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
347 */
348 public Bitmap getBitmap() {
349 return mBitmap;
350 }
351
352 /**
353 * Returns the requested URL for this container.
354 */
355 public String getRequestUrl() {
356 return mRequestUrl;
357 }
358 }
359
360 /**
361 * Wrapper class used to map a Request to the set of active ImageContainer objects that are
362 * interested in its results.
363 */
364 private class BatchedImageRequest {
365 /** The request being tracked */
366 private final Request<?> mRequest;
367
368 /** The result of the request being tracked by this item */
369 private Bitmap mResponseBitmap;
370
Cameron Ketchamdc535522013-05-23 17:40:08 -0400371 /** Error if one occurred for this response */
372 private VolleyError mError;
373
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700374 /** List of all of the active ImageContainers that are interested in the request */
375 private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
376
377 /**
378 * Constructs a new BatchedImageRequest object
379 * @param request The request being tracked
380 * @param container The ImageContainer of the person who initiated the request.
381 */
382 public BatchedImageRequest(Request<?> request, ImageContainer container) {
383 mRequest = request;
384 mContainers.add(container);
385 }
386
387 /**
Cameron Ketchamdc535522013-05-23 17:40:08 -0400388 * Set the error for this response
389 */
390 public void setError(VolleyError error) {
391 mError = error;
392 }
393
394 /**
395 * Get the error for this response
396 */
397 public VolleyError getError() {
398 return mError;
399 }
400
401 /**
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700402 * Adds another ImageContainer to the list of those interested in the results of
403 * the request.
404 */
405 public void addContainer(ImageContainer container) {
406 mContainers.add(container);
407 }
408
409 /**
410 * Detatches the bitmap container from the request and cancels the request if no one is
411 * left listening.
412 * @param container The container to remove from the list
413 * @return True if the request was canceled, false otherwise.
414 */
415 public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
416 mContainers.remove(container);
417 if (mContainers.size() == 0) {
418 mRequest.cancel();
419 return true;
420 }
421 return false;
422 }
423 }
424
425 /**
426 * Starts the runnable for batched delivery of responses if it is not already started.
427 * @param cacheKey The cacheKey of the response being delivered.
428 * @param request The BatchedImageRequest to be delivered.
429 * @param error The volley error associated with the request (if applicable).
430 */
Cameron Ketchamdc535522013-05-23 17:40:08 -0400431 private void batchResponse(String cacheKey, BatchedImageRequest request) {
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700432 mBatchedResponses.put(cacheKey, request);
433 // If we don't already have a batch delivery runnable in flight, make a new one.
434 // Note that this will be used to deliver responses to all callers in mBatchedResponses.
435 if (mRunnable == null) {
436 mRunnable = new Runnable() {
437 @Override
438 public void run() {
439 for (BatchedImageRequest bir : mBatchedResponses.values()) {
440 for (ImageContainer container : bir.mContainers) {
441 // If one of the callers in the batched request canceled the request
442 // after the response was received but before it was delivered,
443 // skip them.
444 if (container.mListener == null) {
445 continue;
446 }
Cameron Ketchamdc535522013-05-23 17:40:08 -0400447 if (bir.getError() == null) {
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700448 container.mBitmap = bir.mResponseBitmap;
449 container.mListener.onResponse(container, false);
450 } else {
Cameron Ketchamdc535522013-05-23 17:40:08 -0400451 container.mListener.onErrorResponse(bir.getError());
Aurash Mahbodd62a6162013-05-08 00:26:10 -0700452 }
453 }
454 }
455 mBatchedResponses.clear();
456 mRunnable = null;
457 }
458
459 };
460 // Post the runnable.
461 mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
462 }
463 }
464
465 private void throwIfNotOnMainThread() {
466 if (Looper.myLooper() != Looper.getMainLooper()) {
467 throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
468 }
469 }
470 /**
471 * Creates a cache key for use with the L1 cache.
472 * @param url The URL of the request.
473 * @param maxWidth The max-width of the output.
474 * @param maxHeight The max-height of the output.
475 */
476 private static String getCacheKey(String url, int maxWidth, int maxHeight) {
477 return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
478 .append("#H").append(maxHeight).append(url).toString();
479 }
480}