• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /**
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  */
16 package com.android.volley.toolbox;
17 
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;
29 
30 import java.util.HashMap;
31 import java.util.LinkedList;
32 
33 /**
34  * Helper that handles loading and caching images from remote URLs.
35  *
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.
41  */
42 public class ImageLoader {
43     /** RequestQueue for dispatching ImageRequests onto. */
44     private final RequestQueue mRequestQueue;
45 
46     /** Amount of time to wait after first response arrives before delivering all responses. */
47     private int mBatchResponseDelayMs = 100;
48 
49     /** The cache implementation to be used as an L1 cache before calling into volley. */
50     private final ImageCache mCache;
51 
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.
55      */
56     private final HashMap<String, BatchedImageRequest> mInFlightRequests =
57             new HashMap<String, BatchedImageRequest>();
58 
59     /** HashMap of the currently pending responses (waiting to be delivered). */
60     private final HashMap<String, BatchedImageRequest> mBatchedResponses =
61             new HashMap<String, BatchedImageRequest>();
62 
63     /** Handler to the main thread. */
64     private final Handler mHandler = new Handler(Looper.getMainLooper());
65 
66     /** Runnable for in-flight response delivery. */
67     private Runnable mRunnable;
68 
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.
73      */
74     public interface ImageCache {
getBitmap(String url)75         public Bitmap getBitmap(String url);
putBitmap(String url, Bitmap bitmap)76         public void putBitmap(String url, Bitmap bitmap);
77     }
78 
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.
83      */
ImageLoader(RequestQueue queue, ImageCache imageCache)84     public ImageLoader(RequestQueue queue, ImageCache imageCache) {
85         mRequestQueue = queue;
86         mCache = imageCache;
87     }
88 
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.
96      */
getImageListener(final ImageView view, final int defaultImageResId, final int errorImageResId)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);
104                 }
105             }
106 
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);
113                 }
114             }
115         };
116     }
117 
118     /**
119      * Interface for the response handlers on image requests.
120      *
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.
125      *
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.
130      */
131     public interface ImageListener extends ErrorListener {
132         /**
133          * Listens for non-error changes to the loading of the image request.
134          *
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.
141          */
142         public void onResponse(ImageContainer response, boolean isImmediate);
143     }
144 
145     /**
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.
151      */
152     public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
153         return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
154     }
155 
156     /**
157      * Checks if the item is available in the cache.
158      *
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.
164      */
165     public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
166         throwIfNotOnMainThread();
167 
168         String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
169         return mCache.getBitmap(cacheKey) != null;
170     }
171 
172     /**
173      * Returns an ImageContainer for the requested URL.
174      *
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.
178      *
179      * @param requestUrl The URL of the image to be loaded.
180      */
181     public ImageContainer get(String requestUrl, final ImageListener listener) {
182         return get(requestUrl, listener, 0, 0);
183     }
184 
185     /**
186      * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with
187      * {@code Scaletype == ScaleType.CENTER_INSIDE}.
188      */
189     public ImageContainer get(String requestUrl, ImageListener imageListener,
190             int maxWidth, int maxHeight) {
191         return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
192     }
193 
194     /**
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).
206      */
207     public ImageContainer get(String requestUrl, ImageListener imageListener,
208             int maxWidth, int maxHeight, ScaleType scaleType) {
209 
210         // only fulfill requests that were initiated from the main thread.
211         throwIfNotOnMainThread();
212 
213         final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
214 
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;
222         }
223 
224         // The bitmap did not exist in the cache, fetch it!
225         ImageContainer imageContainer =
226                 new ImageContainer(null, requestUrl, cacheKey, imageListener);
227 
228         // Update the caller to let them know that they should use the default bitmap.
229         imageListener.onResponse(imageContainer, true);
230 
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;
237         }
238 
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);
243 
244         mRequestQueue.add(newRequest);
245         mInFlightRequests.put(cacheKey,
246                 new BatchedImageRequest(newRequest, imageContainer));
247         return imageContainer;
248     }
249 
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);
256             }
257         }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() {
258             @Override
259             public void onErrorResponse(VolleyError error) {
260                 onGetImageError(cacheKey, error);
261             }
262         });
263     }
264 
265     /**
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.
269      */
270     public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
271         mBatchResponseDelayMs = newBatchedResponseDelayMs;
272     }
273 
274     /**
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.
278      */
279     protected void onGetImageSuccess(String cacheKey, Bitmap response) {
280         // cache the image that was fetched.
281         mCache.putBitmap(cacheKey, response);
282 
283         // remove the request from the list of in-flight requests.
284         BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
285 
286         if (request != null) {
287             // Update the response bitmap.
288             request.mResponseBitmap = response;
289 
290             // Send the batched response
291             batchResponse(cacheKey, request);
292         }
293     }
294 
295     /**
296      * Handler for when an image failed to load.
297      * @param cacheKey The cache key that is associated with the image request.
298      */
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);
303 
304         if (request != null) {
305             // Set the error for this request
306             request.setError(error);
307 
308             // Send the batched response
309             batchResponse(cacheKey, request);
310         }
311     }
312 
313     /**
314      * Container object for all of the data surrounding an image request.
315      */
316     public class ImageContainer {
317         /**
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).
320          */
321         private Bitmap mBitmap;
322 
323         private final ImageListener mListener;
324 
325         /** The cache key that was associated with the request */
326         private final String mCacheKey;
327 
328         /** The request URL that was specified */
329         private final String mRequestUrl;
330 
331         /**
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.
336          */
337         public ImageContainer(Bitmap bitmap, String requestUrl,
338                 String cacheKey, ImageListener listener) {
339             mBitmap = bitmap;
340             mRequestUrl = requestUrl;
341             mCacheKey = cacheKey;
342             mListener = listener;
343         }
344 
345         /**
346          * Releases interest in the in-flight request (and cancels it if no one else is listening).
347          */
348         public void cancelRequest() {
349             if (mListener == null) {
350                 return;
351             }
352 
353             BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
354             if (request != null) {
355                 boolean canceled = request.removeContainerAndCancelIfNecessary(this);
356                 if (canceled) {
357                     mInFlightRequests.remove(mCacheKey);
358                 }
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);
366                     }
367                 }
368             }
369         }
370 
371         /**
372          * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
373          */
374         public Bitmap getBitmap() {
375             return mBitmap;
376         }
377 
378         /**
379          * Returns the requested URL for this container.
380          */
381         public String getRequestUrl() {
382             return mRequestUrl;
383         }
384     }
385 
386     /**
387      * Wrapper class used to map a Request to the set of active ImageContainer objects that are
388      * interested in its results.
389      */
390     private class BatchedImageRequest {
391         /** The request being tracked */
392         private final Request<?> mRequest;
393 
394         /** The result of the request being tracked by this item */
395         private Bitmap mResponseBitmap;
396 
397         /** Error if one occurred for this response */
398         private VolleyError mError;
399 
400         /** List of all of the active ImageContainers that are interested in the request */
401         private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
402 
403         /**
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.
407          */
408         public BatchedImageRequest(Request<?> request, ImageContainer container) {
409             mRequest = request;
410             mContainers.add(container);
411         }
412 
413         /**
414          * Set the error for this response
415          */
416         public void setError(VolleyError error) {
417             mError = error;
418         }
419 
420         /**
421          * Get the error for this response
422          */
423         public VolleyError getError() {
424             return mError;
425         }
426 
427         /**
428          * Adds another ImageContainer to the list of those interested in the results of
429          * the request.
430          */
431         public void addContainer(ImageContainer container) {
432             mContainers.add(container);
433         }
434 
435         /**
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.
440          */
441         public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
442             mContainers.remove(container);
443             if (mContainers.size() == 0) {
444                 mRequest.cancel();
445                 return true;
446             }
447             return false;
448         }
449     }
450 
451     /**
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.
455      */
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;
471                             }
472                             if (bir.getError() == null) {
473                                 container.mBitmap = bir.mResponseBitmap;
474                                 container.mListener.onResponse(container, false);
475                             } else {
476                                 container.mListener.onErrorResponse(bir.getError());
477                             }
478                         }
479                     }
480                     mBatchedResponses.clear();
481                     mRunnable = null;
482                 }
483 
484             };
485             // Post the runnable.
486             mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
487         }
488     }
489 
490     private void throwIfNotOnMainThread() {
491         if (Looper.myLooper() != Looper.getMainLooper()) {
492             throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
493         }
494     }
495     /**
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.
501      */
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();
506     }
507 }
508