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 */ 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; 23import android.widget.ImageView.ScaleType; 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; 29 30import java.util.HashMap; 31import 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 */ 42public 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 { 75 public Bitmap getBitmap(String url); 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 */ 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 */ 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