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; 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 /** 147 * 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 /** 161 * 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 265 batchResponse(cacheKey, request); 266 } 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) { 279 // Set the error for this request 280 request.setError(error); 281 282 // Send the batched response 283 batchResponse(cacheKey, request); 284 } 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 371 /** Error if one occurred for this response */ 372 private VolleyError mError; 373 374 /** 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 /** 388 * 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 /** 402 * 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 */ 431 private void batchResponse(String cacheKey, BatchedImageRequest request) { 432 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 } 447 if (bir.getError() == null) { 448 container.mBitmap = bir.mResponseBitmap; 449 container.mListener.onResponse(container, false); 450 } else { 451 container.mListener.onErrorResponse(bir.getError()); 452 } 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} 481