content: /**
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package com.cjj.volley.toolbox;
  
  import android.graphics.Bitmap;
  import android.graphics.Bitmap.Config;
  import android.os.Handler;
  import android.os.Looper;
  import android.widget.ImageView;
  
  import com.cjj.volley.Request;
  import com.cjj.volley.RequestQueue;
  import com.cjj.volley.VolleyError;
  import com.cjj.volley.Response.ErrorListener;
  import com.cjj.volley.Response.Listener;
  import com.cjj.volley.toolbox.ImageRequest;
  
  import java.util.HashMap;
  import java.util.LinkedList;
  
  /**
   * Helper that handles loading and caching images from remote URLs.
   *
   * The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)}
   * and to pass in the default image listener provided by
   * {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to
   * this class must be made from the main thead, and all responses will be delivered to the main
   * thread as well.
   */
  public class ImageLoader {
      /** RequestQueue for dispatching ImageRequests onto. */
      private final RequestQueue mRequestQueue;
  
      /** Amount of time to wait after first response arrives before delivering all responses. */
      private int mBatchResponseDelayMs = 100;
  
      /** The cache implementation to be used as an L1 cache before calling into volley. */
      private final ImageCache mCache;
  
      /**
       * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so
       * that we can coalesce multiple requests to the same URL into a single network request.
       */
      private final HashMap<String, BatchedImageRequest> mInFlightRequests =
              new HashMap<String, BatchedImageRequest>();
  
      /** HashMap of the currently pending responses (waiting to be delivered). */
      private final HashMap<String, BatchedImageRequest> mBatchedResponses =
              new HashMap<String, BatchedImageRequest>();
  
      /** Handler to the main thread. */
      private final Handler mHandler = new Handler(Looper.getMainLooper());
  
      /** Runnable for in-flight response delivery. */
      private Runnable mRunnable;
  
      /**
       * Simple cache adapter interface. If provided to the ImageLoader, it
       * will be used as an L1 cache before dispatch to Volley. Implementations
       * must not block. Implementation with an LruCache is recommended.
       */
      public interface ImageCache {
          public Bitmap getBitmap(String url);
          public void putBitmap(String url, Bitmap bitmap);
      }
  
      /**
       * Constructs a new ImageLoader.
       * @param queue The RequestQueue to use for making image requests.
       * @param imageCache The cache to use as an L1 cache.
       */
      public ImageLoader(RequestQueue queue, ImageCache imageCache) {
          mRequestQueue = queue;
          mCache = imageCache;
      }
  
      /**
       * The default implementation of ImageListener which handles basic functionality
       * of showing a default image until the network response is received, at which point
       * it will switch to either the actual image or the error image.
       * @param imageView The imageView that the listener is associated with.
       * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist.
       * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist.
       */
      public static ImageListener getImageListener(final ImageView view,
              final int defaultImageResId, final int errorImageResId) {
          return new ImageListener() {
              @Override
              public void onErrorResponse(VolleyError error) {
                  if (errorImageResId != 0) {
                      view.setImageResource(errorImageResId);
                  }
              }
  
              @Override
              public void onResponse(ImageContainer response, boolean isImmediate) {
                  if (response.getBitmap() != null) {
                      view.setImageBitmap(response.getBitmap());
                  } else if (defaultImageResId != 0) {
                      view.setImageResource(defaultImageResId);
                  }
              }
          };
      }
  
      /**
       * Interface for the response handlers on image requests.
       *
       * The call flow is this:
       * 1. Upon being  attached to a request, onResponse(response, true) will
       * be invoked to reflect any cached data that was already available. If the
       * data was available, response.getBitmap() will be non-null.
       *
       * 2. After a network response returns, only one of the following cases will happen:
       *   - onResponse(response, false) will be called if the image was loaded.
       *   or
       *   - onErrorResponse will be called if there was an error loading the image.
       */
      public interface ImageListener extends ErrorListener {
          /**
           * Listens for non-error changes to the loading of the image request.
           *
           * @param response Holds all information pertaining to the request, as well
           * as the bitmap (if it is loaded).
           * @param isImmediate True if this was called during ImageLoader.get() variants.
           * This can be used to differentiate between a cached image loading and a network
           * image loading in order to, for example, run an animation to fade in network loaded
           * images.
           */
          public void onResponse(ImageContainer response, boolean isImmediate);
      }
  
      /**
       * Checks if the item is available in the cache.
       * @param requestUrl The url of the remote image
       * @param maxWidth The maximum width of the returned image.
       * @param maxHeight The maximum height of the returned image.
       * @return True if the item exists in cache, false otherwise.
       */
      public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
          throwIfNotOnMainThread();
  
          String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
          return mCache.getBitmap(cacheKey) != null;
      }
  
      /**
       * Returns an ImageContainer for the requested URL.
       *
       * The ImageContainer will contain either the specified default bitmap or the loaded bitmap.
       * If the default was returned, the {@link ImageLoader} will be invoked when the
       * request is fulfilled.
       *
       * @param requestUrl The URL of the image to be loaded.
       * @param defaultImage Optional default image to return until the actual image is loaded.
       */
      public ImageContainer get(String requestUrl, final ImageListener listener) {
          return get(requestUrl, listener, 0, 0);
      }
  
      /**
       * Issues a bitmap request with the given URL if that image is not available
       * in the cache, and returns a bitmap container that contains all of the data
       * relating to the request (as well as the default image if the requested
       * image is not available).
       * @param requestUrl The url of the remote image
       * @param imageListener The listener to call when the remote image is loaded
       * @param maxWidth The maximum width of the returned image.
       * @param maxHeight The maximum height of the returned image.
       * @return A container object that contains all of the properties of the request, as well as
       *     the currently available image (default if remote is not loaded).
       */
      public ImageContainer get(String requestUrl, ImageListener imageListener,
              int maxWidth, int maxHeight) {
          // only fulfill requests that were initiated from the main thread.
          throwIfNotOnMainThread();
  
          final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);
  
          // Try to look up the request in the cache of remote images.
          Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
          if (cachedBitmap != null) {
              // Return the cached bitmap.
              ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
              imageListener.onResponse(container, true);
              return container;
          }
  
          // The bitmap did not exist in the cache, fetch it!
          ImageContainer imageContainer =
                  new ImageContainer(null, requestUrl, cacheKey, imageListener);
  
          // Update the caller to let them know that they should use the default bitmap.
          imageListener.onResponse(imageContainer, true);
  
          // Check to see if a request is already in-flight.
          BatchedImageRequest request = mInFlightRequests.get(cacheKey);
          if (request != null) {
              // If it is, add this request to the list of listeners.
              request.addContainer(imageContainer);
              return imageContainer;
          }
  
          // The request is not already in flight. Send the new request to the network and
          // track it.
          Request<?> newRequest =
              new ImageRequest(requestUrl, new Listener<Bitmap>() {
                  @Override
                  public void onResponse(Bitmap response) {
                      onGetImageSuccess(cacheKey, response);
                  }
              }, maxWidth, maxHeight,
              Config.RGB_565, new ErrorListener() {
                  @Override
                  public void onErrorResponse(VolleyError error) {
                      onGetImageError(cacheKey, error);
                  }
              });
  
          mRequestQueue.add(newRequest);
          mInFlightRequests.put(cacheKey,
                  new BatchedImageRequest(newRequest, imageContainer));
          return imageContainer;
      }
  
      /**
       * Sets the amount of time to wait after the first response arrives before delivering all
       * responses. Batching can be disabled entirely by passing in 0.
       * @param newBatchedResponseDelayMs The time in milliseconds to wait.
       */
      public void setBatchedResponseDelay(int newBatchedResponseDelayMs) {
          mBatchResponseDelayMs = newBatchedResponseDelayMs;
      }
  
      /**
       * Handler for when an image was successfully loaded.
       * @param cacheKey The cache key that is associated with the image request.
       * @param response The bitmap that was returned from the network.
       */
      private void onGetImageSuccess(String cacheKey, Bitmap response) {
          // cache the image that was fetched.
          mCache.putBitmap(cacheKey, response);
  
          // remove the request from the list of in-flight requests.
          BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
  
          if (request != null) {
              // Update the response bitmap.
              request.mResponseBitmap = response;
  
              // Send the batched response
              batchResponse(cacheKey, request);
          }
      }
  
      /**
       * Handler for when an image failed to load.
       * @param cacheKey The cache key that is associated with the image request.
       */
      private void onGetImageError(String cacheKey, VolleyError error) {
          // Notify the requesters that something failed via a null result.
          // Remove this request from the list of in-flight requests.
          BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
  
          // Set the error for this request
          request.setError(error);
  
          if (request != null) {
              // Send the batched response
              batchResponse(cacheKey, request);
          }
      }
  
      /**
       * Container object for all of the data surrounding an image request.
       */
      public class ImageContainer {
          /**
           * The most relevant bitmap for the container. If the image was in cache, the
           * Holder to use for the final bitmap (the one that pairs to the requested URL).
           */
          private Bitmap mBitmap;
  
          private final ImageListener mListener;
  
          /** The cache key that was associated with the request */
          private final String mCacheKey;
  
          /** The request URL that was specified */
          private final String mRequestUrl;
  
          /**
           * Constructs a BitmapContainer object.
           * @param bitmap The final bitmap (if it exists).
           * @param requestUrl The requested URL for this container.
           * @param cacheKey The cache key that identifies the requested URL for this container.
           */
          public ImageContainer(Bitmap bitmap, String requestUrl,
                  String cacheKey, ImageListener listener) {
              mBitmap = bitmap;
              mRequestUrl = requestUrl;
              mCacheKey = cacheKey;
              mListener = listener;
          }
  
          /**
           * Releases interest in the in-flight request (and cancels it if no one else is listening).
           */
          public void cancelRequest() {
              if (mListener == null) {
                  return;
              }
  
              BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
              if (request != null) {
                  boolean canceled = request.removeContainerAndCancelIfNecessary(this);
                  if (canceled) {
                      mInFlightRequests.remove(mCacheKey);
                  }
              } else {
                  // check to see if it is already batched for delivery.
                  request = mBatchedResponses.get(mCacheKey);
                  if (request != null) {
                      request.removeContainerAndCancelIfNecessary(this);
                      if (request.mContainers.size() == 0) {
                          mBatchedResponses.remove(mCacheKey);
                      }
                  }
              }
          }
  
          /**
           * Returns the bitmap associated with the request URL if it has been loaded, null otherwise.
           */
          public Bitmap getBitmap() {
              return mBitmap;
          }
  
          /**
           * Returns the requested URL for this container.
           */
          public String getRequestUrl() {
              return mRequestUrl;
          }
      }
  
      /**
       * Wrapper class used to map a Request to the set of active ImageContainer objects that are
       * interested in its results.
       */
      private class BatchedImageRequest {
          /** The request being tracked */
          private final Request<?> mRequest;
  
          /** The result of the request being tracked by this item */
          private Bitmap mResponseBitmap;
  
          /** Error if one occurred for this response */
          private VolleyError mError;
  
          /** List of all of the active ImageContainers that are interested in the request */
          private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();
  
          /**
           * Constructs a new BatchedImageRequest object
           * @param request The request being tracked
           * @param container The ImageContainer of the person who initiated the request.
           */
          public BatchedImageRequest(Request<?> request, ImageContainer container) {
              mRequest = request;
              mContainers.add(container);
          }
  
          /**
           * Set the error for this response
           */
          public void setError(VolleyError error) {
              mError = error;
          }
  
          /**
           * Get the error for this response
           */
          public VolleyError getError() {
              return mError;
          }
  
          /**
           * Adds another ImageContainer to the list of those interested in the results of
           * the request.
           */
          public void addContainer(ImageContainer container) {
              mContainers.add(container);
          }
  
          /**
           * Detatches the bitmap container from the request and cancels the request if no one is
           * left listening.
           * @param container The container to remove from the list
           * @return True if the request was canceled, false otherwise.
           */
          public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
              mContainers.remove(container);
              if (mContainers.size() == 0) {
                  mRequest.cancel();
                  return true;
              }
              return false;
          }
      }
  
      /**
       * Starts the runnable for batched delivery of responses if it is not already started.
       * @param cacheKey The cacheKey of the response being delivered.
       * @param request The BatchedImageRequest to be delivered.
       * @param error The volley error associated with the request (if applicable).
       */
      private void batchResponse(String cacheKey, BatchedImageRequest request) {
          mBatchedResponses.put(cacheKey, request);
          // If we don't already have a batch delivery runnable in flight, make a new one.
          // Note that this will be used to deliver responses to all callers in mBatchedResponses.
          if (mRunnable == null) {
              mRunnable = new Runnable() {
                  @Override
                  public void run() {
                      for (BatchedImageRequest bir : mBatchedResponses.values()) {
                          for (ImageContainer container : bir.mContainers) {
                              // If one of the callers in the batched request canceled the request
                              // after the response was received but before it was delivered,
                              // skip them.
                              if (container.mListener == null) {
                                  continue;
                              }
                              if (bir.getError() == null) {
                                  container.mBitmap = bir.mResponseBitmap;
                                  container.mListener.onResponse(container, false);
                              } else {
                                  container.mListener.onErrorResponse(bir.getError());
                              }
                          }
                      }
                      mBatchedResponses.clear();
                      mRunnable = null;
                  }
  
              };
              // Post the runnable.
              mHandler.postDelayed(mRunnable, mBatchResponseDelayMs);
          }
      }
  
      private void throwIfNotOnMainThread() {
          if (Looper.myLooper() != Looper.getMainLooper()) {
              throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
          }
      }
      /**
       * Creates a cache key for use with the L1 cache.
       * @param url The URL of the request.
       * @param maxWidth The max-width of the output.
       * @param maxHeight The max-height of the output.
       */
      private static String getCacheKey(String url, int maxWidth, int maxHeight) {
          return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
                  .append("#H").append(maxHeight).append(url).toString();
      }
  }
  