コード例 #1
0
public final class UrlImageViewHelper {

  public static int copyStream(InputStream input, OutputStream output) throws IOException {
    byte[] stuff = new byte[1024];
    int read = 0;
    int total = 0;
    while ((read = input.read(stuff)) != -1) {
      output.write(stuff, 0, read);
      total += read;
    }
    return total;
  }

  static Resources mResources;
  static DisplayMetrics mMetrics;

  private static void prepareResources(Context context) {
    if (mMetrics != null) return;
    mMetrics = new DisplayMetrics();
    Activity act = (Activity) context;
    act.getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
    AssetManager mgr = context.getAssets();
    mResources = new Resources(mgr, mMetrics, context.getResources().getConfiguration());
  }

  private static Drawable loadDrawableFromStream(
      Context context, String url, String filename, int targetWidth, int targetHeight) {
    prepareResources(context);

    //        Log.v(Constants.LOGTAG,targetWidth);
    //        Log.v(Constants.LOGTAG,targetHeight);
    try {
      Options o = new Options();
      o.inJustDecodeBounds = true;
      FileInputStream stream = new FileInputStream(filename);
      BitmapFactory.decodeStream(stream, null, o);
      stream.close();
      stream = new FileInputStream(filename);
      int scale = 0;
      while ((o.outWidth >> scale) > targetWidth || (o.outHeight >> scale) > targetHeight) {
        Log.v(Constants.LOGTAG, "downsampling");
        scale++;
      }
      o = new Options();
      o.inSampleSize = 1 << scale;
      Bitmap bitmap = BitmapFactory.decodeStream(stream, null, o);
      if (Constants.LOG_ENABLED)
        Log.i(
            Constants.LOGTAG,
            String.format("Loaded bitmap (%dx%d).", bitmap.getWidth(), bitmap.getHeight()));
      BitmapDrawable bd = new BitmapDrawable(mResources, bitmap);
      return new ZombieDrawable(url, bd);
    } catch (IOException e) {
      return null;
    }
  }

  public static final int CACHE_DURATION_INFINITE = Integer.MAX_VALUE;
  public static final int CACHE_DURATION_ONE_DAY = 1000 * 60 * 60 * 24;
  public static final int CACHE_DURATION_TWO_DAYS = CACHE_DURATION_ONE_DAY * 2;
  public static final int CACHE_DURATION_THREE_DAYS = CACHE_DURATION_ONE_DAY * 3;
  public static final int CACHE_DURATION_FOUR_DAYS = CACHE_DURATION_ONE_DAY * 4;
  public static final int CACHE_DURATION_FIVE_DAYS = CACHE_DURATION_ONE_DAY * 5;
  public static final int CACHE_DURATION_SIX_DAYS = CACHE_DURATION_ONE_DAY * 6;
  public static final int CACHE_DURATION_ONE_WEEK = CACHE_DURATION_ONE_DAY * 7;

  public static void setUrlDrawable(ImageView imageView, String url, int defaultResource) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, defaultResource, CACHE_DURATION_THREE_DAYS);
  }

  public static void setUrlDrawable(ImageView imageView, String url) {
    setUrlDrawable(imageView.getContext(), imageView, url, null, CACHE_DURATION_THREE_DAYS, null);
  }

  public static void loadUrlDrawable(Context context, String url) {
    setUrlDrawable(context, null, url, null, CACHE_DURATION_THREE_DAYS, null);
  }

  public static void setUrlDrawable(ImageView imageView, String url, Drawable defaultDrawable) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, defaultDrawable, CACHE_DURATION_THREE_DAYS, null);
  }

  public static void setUrlDrawable(
      ImageView imageView, String url, int defaultResource, long cacheDurationMs) {
    setUrlDrawable(imageView.getContext(), imageView, url, defaultResource, cacheDurationMs);
  }

  public static void loadUrlDrawable(Context context, String url, long cacheDurationMs) {
    setUrlDrawable(context, null, url, null, cacheDurationMs, null);
  }

  public static void setUrlDrawable(
      ImageView imageView, String url, Drawable defaultDrawable, long cacheDurationMs) {
    setUrlDrawable(imageView.getContext(), imageView, url, defaultDrawable, cacheDurationMs, null);
  }

  private static void setUrlDrawable(
      Context context, ImageView imageView, String url, int defaultResource, long cacheDurationMs) {
    Drawable d = null;
    if (defaultResource != 0) d = imageView.getResources().getDrawable(defaultResource);
    setUrlDrawable(context, imageView, url, d, cacheDurationMs, null);
  }

  public static void setUrlDrawable(
      ImageView imageView, String url, int defaultResource, UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(),
        imageView,
        url,
        defaultResource,
        CACHE_DURATION_THREE_DAYS,
        callback);
  }

  public static void setUrlDrawable(
      ImageView imageView, String url, UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, null, CACHE_DURATION_THREE_DAYS, callback);
  }

  public static void loadUrlDrawable(Context context, String url, UrlImageViewCallback callback) {
    setUrlDrawable(context, null, url, null, CACHE_DURATION_THREE_DAYS, callback);
  }

  public static void setUrlDrawable(
      ImageView imageView, String url, Drawable defaultDrawable, UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(),
        imageView,
        url,
        defaultDrawable,
        CACHE_DURATION_THREE_DAYS,
        callback);
  }

  public static void setUrlDrawable(
      ImageView imageView,
      String url,
      int defaultResource,
      long cacheDurationMs,
      UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, defaultResource, cacheDurationMs, callback);
  }

  public static void loadUrlDrawable(
      Context context, String url, long cacheDurationMs, UrlImageViewCallback callback) {
    setUrlDrawable(context, null, url, null, cacheDurationMs, callback);
  }

  public static void setUrlDrawable(
      ImageView imageView,
      String url,
      Drawable defaultDrawable,
      long cacheDurationMs,
      UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, defaultDrawable, cacheDurationMs, callback);
  }

  private static void setUrlDrawable(
      Context context,
      ImageView imageView,
      String url,
      int defaultResource,
      long cacheDurationMs,
      UrlImageViewCallback callback) {
    Drawable d = null;
    if (defaultResource != 0) d = imageView.getResources().getDrawable(defaultResource);
    setUrlDrawable(context, imageView, url, d, cacheDurationMs, callback);
  }

  private static boolean isNullOrEmpty(CharSequence s) {
    return (s == null || s.equals("") || s.equals("null") || s.equals("NULL"));
  }

  private static boolean mHasCleaned = false;

  public static String getFilenameForUrl(String url) {
    return "" + url.hashCode() + ".urlimage";
  }

  private static void cleanup(Context context) {
    if (mHasCleaned) return;
    mHasCleaned = true;
    try {
      // purge any *.urlimage files over a week old
      String[] files = context.getFilesDir().list();
      if (files == null) return;
      for (String file : files) {
        if (!file.endsWith(".urlimage")) continue;

        File f = new File(context.getFilesDir().getAbsolutePath() + '/' + file);
        if (System.currentTimeMillis() > f.lastModified() + CACHE_DURATION_ONE_WEEK) f.delete();
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private static void setUrlDrawable(
      final Context context,
      ImageView imageView,
      final String url,
      final Drawable defaultDrawable,
      long cacheDurationMs,
      final UrlImageViewCallback callback) {
    cleanup(context);
    // disassociate this ImageView from any pending downloads
    if (isNullOrEmpty(url)) {
      if (imageView != null) imageView.setImageDrawable(defaultDrawable);
      return;
    }

    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = wm.getDefaultDisplay();
    int tw = display.getWidth();
    int th = display.getHeight();

    if (mDeadCache == null) mDeadCache = new UrlLruCache(getHeapSize(context) / 8);
    Drawable drawable;
    BitmapDrawable zd = mDeadCache.remove(url);
    if (zd != null) {
      // this drawable was resurrected, it should not be in the live cache
      if (Constants.LOG_ENABLED) Log.i(Constants.LOGTAG, "zombie load");
      Assert.assertTrue(!mAllCache.contains(zd));
      drawable = new ZombieDrawable(url, zd);
    } else {
      drawable = mLiveCache.get(url);
    }

    if (drawable != null) {
      if (Constants.LOG_ENABLED) Log.i(Constants.LOGTAG, "Cache hit on: " + url);
      if (imageView != null) imageView.setImageDrawable(drawable);
      if (callback != null) callback.onLoaded(imageView, drawable, url, true);
      return;
    }

    // oh noes, at this point we definitely do not have the file available in memory
    // let's prepare for an asynchronous load of the image.

    final String filename = context.getFileStreamPath(getFilenameForUrl(url)).getAbsolutePath();

    // null it while it is downloading
    if (imageView != null) imageView.setImageDrawable(defaultDrawable);

    // since listviews reuse their views, we need to
    // take note of which url this view is waiting for.
    // This may change rapidly as the list scrolls or is filtered, etc.
    if (Constants.LOG_ENABLED) Log.i(Constants.LOGTAG, "Waiting for " + url);
    if (imageView != null) mPendingViews.put(imageView, url);

    ArrayList<ImageView> currentDownload = mPendingDownloads.get(url);
    if (currentDownload != null) {
      // Also, multiple vies may be waiting for this url.
      // So, let's maintain a list of these views.
      // When the url is downloaded, it sets the imagedrawable for
      // every view in the list. It needs to also validate that
      // the imageview is still waiting for this url.
      if (imageView != null) currentDownload.add(imageView);
      return;
    }

    final ArrayList<ImageView> downloads = new ArrayList<ImageView>();
    if (imageView != null) downloads.add(imageView);
    mPendingDownloads.put(url, downloads);

    final int targetWidth = tw <= 0 ? Integer.MAX_VALUE : tw;
    final int targetHeight = th <= 0 ? Integer.MAX_VALUE : th;
    final Loader loader =
        new Loader() {
          @Override
          public void run() {
            try {
              result = loadDrawableFromStream(context, url, filename, targetWidth, targetHeight);
            } catch (Exception ex) {
            }
          }
        };

    final Runnable completion =
        new Runnable() {
          @Override
          public void run() {
            Assert.assertEquals(Looper.myLooper(), Looper.getMainLooper());
            Drawable usableResult = loader.result;
            if (usableResult == null) usableResult = defaultDrawable;
            mPendingDownloads.remove(url);
            mLiveCache.put(url, usableResult);
            for (ImageView iv : downloads) {
              // validate the url it is waiting for
              String pendingUrl = mPendingViews.get(iv);
              if (!url.equals(pendingUrl)) {
                if (Constants.LOG_ENABLED)
                  Log.i(Constants.LOGTAG, "Ignoring out of date request to update view for " + url);
                continue;
              }
              mPendingViews.remove(iv);
              if (usableResult != null) {
                //                        System.out.println(String.format("imageView: %dx%d,
                // %dx%d", imageView.getMeasuredWidth(), imageView.getMeasuredHeight(),
                // imageView.getWidth(), imageView.getHeight()));
                iv.setImageDrawable(usableResult);
                //                        System.out.println(String.format("imageView: %dx%d,
                // %dx%d", imageView.getMeasuredWidth(), imageView.getMeasuredHeight(),
                // imageView.getWidth(), imageView.getHeight()));
                if (callback != null) callback.onLoaded(iv, loader.result, url, false);
              }
            }
          }
        };

    File file = new File(filename);
    if (file.exists()) {
      try {
        if (cacheDurationMs == CACHE_DURATION_INFINITE
            || System.currentTimeMillis() < file.lastModified() + cacheDurationMs) {
          if (Constants.LOG_ENABLED)
            Log.i(
                Constants.LOGTAG,
                "File Cache hit on: "
                    + url
                    + ". "
                    + (System.currentTimeMillis() - file.lastModified())
                    + "ms old.");

          AsyncTask<Void, Void, Void> fileloader =
              new AsyncTask<Void, Void, Void>() {
                protected Void doInBackground(Void[] params) {
                  loader.run();
                  return null;
                }

                protected void onPostExecute(Void result) {
                  completion.run();
                }
              };
          executeTask(fileloader);
          return;
        } else {
          if (Constants.LOG_ENABLED) Log.i(Constants.LOGTAG, "File cache has expired. Refreshing.");
        }
      } catch (Exception ex) {
      }
    }

    mDownloader.download(context, url, filename, loader, completion);
  }

  private abstract static class Loader implements Runnable {
    public Drawable result;
  }

  public static interface UrlDownloader {
    public void download(
        Context context, String url, String filename, Runnable loader, Runnable completion);
  }

  private static UrlDownloader mDefaultDownloader =
      new UrlDownloader() {
        @Override
        public void download(
            final Context context,
            final String url,
            final String filename,
            final Runnable loader,
            final Runnable completion) {
          AsyncTask<Void, Void, Void> downloader =
              new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                  InputStream is = null;
                  FileOutputStream fos = null;
                  AndroidHttpClient client = null;
                  try {
                    if (url.startsWith(ContactsContract.Contacts.CONTENT_URI.toString())) {
                      ContentResolver cr = context.getContentResolver();
                      is =
                          ContactsContract.Contacts.openContactPhotoInputStream(cr, Uri.parse(url));
                    } else {
                      client = AndroidHttpClient.newInstance(context.getPackageName());
                      HttpGet get = new HttpGet(url);
                      HttpParams httpParams = new BasicHttpParams();
                      HttpClientParams.setRedirecting(httpParams, true);

                      if (mRequestPropertiesCallback != null) {
                        ArrayList<NameValuePair> props =
                            mRequestPropertiesCallback.getHeadersForRequest(context, url);
                        if (props != null) {
                          for (NameValuePair pair : props) {
                            httpParams.setParameter(pair.getName(), pair.getValue());
                          }
                        }
                      }

                      get.setParams(httpParams);
                      HttpResponse resp = client.execute(get);
                      int status = resp.getStatusLine().getStatusCode();

                      if (status != HttpURLConnection.HTTP_OK) {
                        return null;
                      }
                      HttpEntity entity = resp.getEntity();
                      is = entity.getContent();
                    }

                    if (is != null) {
                      fos = new FileOutputStream(filename);
                      copyStream(is, fos);
                    }
                    loader.run();
                    return null;
                  } catch (Throwable e) {
                    Log.e(getClass().getSimpleName(), "Error thrown while getting drawable", e);
                    return null;
                  } finally {
                    if (client != null) {
                      client.close();
                    }
                    if (is != null) {
                      try {
                        is.close();
                      } catch (IOException e) {
                        Log.e(getClass().getSimpleName(), "Failed to close input stream", e);
                      }
                    }
                    if (fos != null) {
                      try {
                        fos.close();
                      } catch (IOException e) {
                        Log.e(getClass().getSimpleName(), "Failed to close file output stream");
                      }
                    }
                  }
                }

                protected void onPostExecute(Void result) {
                  completion.run();
                }
              };
          executeTask(downloader);
        }
      };

  public static interface RequestPropertiesCallback {
    public ArrayList<NameValuePair> getHeadersForRequest(Context context, String url);
  }

  private static RequestPropertiesCallback mRequestPropertiesCallback;

  public static RequestPropertiesCallback getRequestPropertiesCallback() {
    return mRequestPropertiesCallback;
  }

  public static void setRequestPropertiesCallback(RequestPropertiesCallback callback) {
    mRequestPropertiesCallback = callback;
  }

  public static void useDownloader(UrlDownloader downloader) {
    mDownloader = downloader;
  }

  public static void useDefaultDownloader() {
    mDownloader = mDefaultDownloader;
  }

  public static UrlDownloader getDefaultDownloader() {
    return mDownloader;
  }

  private static UrlImageCache mLiveCache = UrlImageCache.getInstance();

  private static UrlLruCache mDeadCache;
  private static HashSet<BitmapDrawable> mAllCache = new HashSet<BitmapDrawable>();

  private static int getHeapSize(Context context) {
    return ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass()
        * 1024
        * 1024;
  }

  private static class ZombieDrawable extends WrapperDrawable {
    public ZombieDrawable(String url, BitmapDrawable drawable) {
      super(drawable);
      mUrl = url;

      mAllCache.add(drawable);
      mDeadCache.remove(url);
      mLiveCache.put(url, this);
    }

    String mUrl;

    @Override
    protected void finalize() throws Throwable {
      super.finalize();

      mDeadCache.put(mUrl, mDrawable);
      mAllCache.remove(mDrawable);
      mLiveCache.remove(mUrl);
      if (Constants.LOG_ENABLED) Log.i(Constants.LOGTAG, "Zombie GC event");
      System.gc();
    }
  }

  private static UrlDownloader mDownloader = mDefaultDownloader;

  private static void executeTask(AsyncTask<Void, Void, Void> task) {
    if (Build.VERSION.SDK_INT < Constants.HONEYCOMB) task.execute();
    else executeTaskHoneycomb(task);
  }

  @TargetApi(Constants.HONEYCOMB)
  private static void executeTaskHoneycomb(AsyncTask<Void, Void, Void> task) {
    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  }

  private static Hashtable<ImageView, String> mPendingViews = new Hashtable<ImageView, String>();
  private static Hashtable<String, ArrayList<ImageView>> mPendingDownloads =
      new Hashtable<String, ArrayList<ImageView>>();
}
コード例 #2
0
public final class UrlImageViewHelper {
  private static void clog(String format, Object... args) {
    String log;
    if (args.length == 0) log = format;
    else log = String.format(format, args);
    if (Constants.LOG_ENABLED) Log.i(Constants.LOGTAG, log);
  }

  public static int copyStream(final InputStream input, final OutputStream output)
      throws IOException {
    final byte[] stuff = new byte[1024];
    int read = 0;
    int total = 0;
    while ((read = input.read(stuff)) != -1) {
      output.write(stuff, 0, read);
      total += read;
    }
    return total;
  }

  static Resources mResources;
  static DisplayMetrics mMetrics;

  private static void prepareResources(final Context context) {
    if (mMetrics != null) {
      return;
    }
    mMetrics = new DisplayMetrics();
    final Activity act = (Activity) context;
    act.getWindowManager().getDefaultDisplay().getMetrics(mMetrics);
    final AssetManager mgr = context.getAssets();
    mResources = new Resources(mgr, mMetrics, context.getResources().getConfiguration());
  }

  private static boolean mUseBitmapScaling = true;
  /**
   * Bitmap scaling will use smart/sane values to limit the maximum dimension of the bitmap during
   * decode. This will prevent any dimension of the bitmap from being larger than the dimensions of
   * the device itself. Doing this will conserve memory.
   *
   * @param useBitmapScaling Toggle for smart resizing.
   */
  public static void setUseBitmapScaling(boolean useBitmapScaling) {
    mUseBitmapScaling = useBitmapScaling;
  }
  /**
   * Bitmap scaling will use smart/sane values to limit the maximum dimension of the bitmap during
   * decode. This will prevent any dimension of the bitmap from being larger than the dimensions of
   * the device itself. Doing this will conserve memory.
   */
  public static boolean getUseBitmapScaling() {
    return mUseBitmapScaling;
  }

  private static Drawable loadDrawableFromStream(
      final Context context,
      final String url,
      final String filename,
      final int targetWidth,
      final int targetHeight) {
    prepareResources(context);

    //        Log.v(Constants.LOGTAG,targetWidth);
    //        Log.v(Constants.LOGTAG,targetHeight);
    FileInputStream stream = null;
    try {
      BitmapFactory.Options o = null;
      if (mUseBitmapScaling) {
        o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        stream = new FileInputStream(filename);
        BitmapFactory.decodeStream(stream, null, o);
        stream.close();
        int scale = 0;
        while ((o.outWidth >> scale) > targetWidth || (o.outHeight >> scale) > targetHeight) {
          scale++;
        }
        o = new Options();
        o.inSampleSize = 1 << scale;
      }
      stream = new FileInputStream(filename);
      final Bitmap bitmap = BitmapFactory.decodeStream(stream, null, o);
      clog(String.format("Loaded bitmap (%dx%d).", bitmap.getWidth(), bitmap.getHeight()));
      final BitmapDrawable bd = new BitmapDrawable(mResources, bitmap);
      return new ZombieDrawable(url, bd);
    } catch (final IOException e) {
      return null;
    } finally {
      if (stream != null) {
        try {
          stream.close();
        } catch (IOException e) {
          Log.w(Constants.LOGTAG, "Failed to close FileInputStream", e);
        }
      }
    }
  }

  public static final int CACHE_DURATION_INFINITE = Integer.MAX_VALUE;
  public static final int CACHE_DURATION_ONE_DAY = 1000 * 60 * 60 * 24;
  public static final int CACHE_DURATION_TWO_DAYS = CACHE_DURATION_ONE_DAY * 2;
  public static final int CACHE_DURATION_THREE_DAYS = CACHE_DURATION_ONE_DAY * 3;
  public static final int CACHE_DURATION_FOUR_DAYS = CACHE_DURATION_ONE_DAY * 4;
  public static final int CACHE_DURATION_FIVE_DAYS = CACHE_DURATION_ONE_DAY * 5;
  public static final int CACHE_DURATION_SIX_DAYS = CACHE_DURATION_ONE_DAY * 6;
  public static final int CACHE_DURATION_ONE_WEEK = CACHE_DURATION_ONE_DAY * 7;

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultResource The Android resid of the {@link Drawable} that should be displayed while
   *     the image is being downloaded.
   */
  public static void setUrlDrawable(
      final ImageView imageView, final String url, final int defaultResource) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, defaultResource, CACHE_DURATION_THREE_DAYS);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView} once it finishes loading.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   */
  public static void setUrlDrawable(final ImageView imageView, final String url) {
    setUrlDrawable(imageView.getContext(), imageView, url, null, CACHE_DURATION_THREE_DAYS, null);
  }

  public static void loadUrlDrawable(final Context context, final String url) {
    setUrlDrawable(context, null, url, null, CACHE_DURATION_THREE_DAYS, null);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultDrawable A {@link Drawable} that should be displayed in {@code imageView} while
   *     the image has not been loaded. This image will also be displayed if the image fails to
   *     load. This can be set to {@code null}.
   */
  public static void setUrlDrawable(
      final ImageView imageView, final String url, final Drawable defaultDrawable) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, defaultDrawable, CACHE_DURATION_THREE_DAYS, null);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultResource The Android resid of the {@link Drawable} that should be displayed while
   *     the image is being downloaded.
   * @param cacheDurationMs The length of time, in milliseconds, that this image should be cached
   *     locally.
   */
  public static void setUrlDrawable(
      final ImageView imageView,
      final String url,
      final int defaultResource,
      final long cacheDurationMs) {
    setUrlDrawable(imageView.getContext(), imageView, url, defaultResource, cacheDurationMs);
  }

  public static void loadUrlDrawable(
      final Context context, final String url, final long cacheDurationMs) {
    setUrlDrawable(context, null, url, null, cacheDurationMs, null);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultDrawable A {@link Drawable} that should be displayed in {@code imageView} while
   *     the image has not been loaded. This image will also be displayed if the image fails to
   *     load. This can be set to {@code null}.
   * @param cacheDurationMs The length of time, in milliseconds, that this image should be cached
   *     locally.
   */
  public static void setUrlDrawable(
      final ImageView imageView,
      final String url,
      final Drawable defaultDrawable,
      final long cacheDurationMs) {
    setUrlDrawable(imageView.getContext(), imageView, url, defaultDrawable, cacheDurationMs, null);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param context A {@link Context} to allow setUrlDrawable to load and save files.
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultResource The Android resid of the {@link Drawable} that should be displayed while
   *     the image is being downloaded.
   * @param cacheDurationMs The length of time, in milliseconds, that this image should be cached
   *     locally.
   */
  private static void setUrlDrawable(
      final Context context,
      final ImageView imageView,
      final String url,
      final int defaultResource,
      final long cacheDurationMs) {
    Drawable d = null;
    if (defaultResource != 0) {
      d = imageView.getResources().getDrawable(defaultResource);
    }
    setUrlDrawable(context, imageView, url, d, cacheDurationMs, null);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultResource The Android resid of the {@link Drawable} that should be displayed while
   *     the image is being downloaded.
   * @param callback An instance of {@link UrlImageViewCallback} that is called when the image
   *     successfully finishes loading. This value can be null.
   */
  public static void setUrlDrawable(
      final ImageView imageView,
      final String url,
      final int defaultResource,
      final UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(),
        imageView,
        url,
        defaultResource,
        CACHE_DURATION_THREE_DAYS,
        callback);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param callback An instance of {@link UrlImageViewCallback} that is called when the image
   *     successfully finishes loading. This value can be null.
   */
  public static void setUrlDrawable(
      final ImageView imageView, final String url, final UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, null, CACHE_DURATION_THREE_DAYS, callback);
  }

  public static void loadUrlDrawable(
      final Context context, final String url, final UrlImageViewCallback callback) {
    setUrlDrawable(context, null, url, null, CACHE_DURATION_THREE_DAYS, callback);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultDrawable A {@link Drawable} that should be displayed in {@code imageView} while
   *     the image has not been loaded. This image will also be displayed if the image fails to
   *     load. This can be set to {@code null}.
   * @param callback An instance of {@link UrlImageViewCallback} that is called when the image
   *     successfully finishes loading. This value can be null.
   */
  public static void setUrlDrawable(
      final ImageView imageView,
      final String url,
      final Drawable defaultDrawable,
      final UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(),
        imageView,
        url,
        defaultDrawable,
        CACHE_DURATION_THREE_DAYS,
        callback);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultResource The Android resid of the {@link Drawable} that should be displayed while
   *     the image is being downloaded.
   * @param cacheDurationMs The length of time, in milliseconds, that this image should be cached
   *     locally.
   * @param callback An instance of {@link UrlImageViewCallback} that is called when the image
   *     successfully finishes loading. This value can be null.
   */
  public static void setUrlDrawable(
      final ImageView imageView,
      final String url,
      final int defaultResource,
      final long cacheDurationMs,
      final UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, defaultResource, cacheDurationMs, callback);
  }

  public static void loadUrlDrawable(
      final Context context,
      final String url,
      final long cacheDurationMs,
      final UrlImageViewCallback callback) {
    setUrlDrawable(context, null, url, null, cacheDurationMs, callback);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultDrawable A {@link Drawable} that should be displayed in {@code imageView} while
   *     the image has not been loaded. This image will also be displayed if the image fails to
   *     load. This can be set to {@code null}.
   * @param cacheDurationMs The length of time, in milliseconds, that this image should be cached
   *     locally.
   * @param callback An instance of {@link UrlImageViewCallback} that is called when the image
   *     successfully finishes loading. This value can be null.
   */
  public static void setUrlDrawable(
      final ImageView imageView,
      final String url,
      final Drawable defaultDrawable,
      final long cacheDurationMs,
      final UrlImageViewCallback callback) {
    setUrlDrawable(
        imageView.getContext(), imageView, url, defaultDrawable, cacheDurationMs, callback);
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param context A {@link Context} to allow setUrlDrawable to load and save files.
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultResource The Android resid of the {@link Drawable} that should be displayed while
   *     the image is being downloaded.
   * @param cacheDurationMs The length of time, in milliseconds, that this image should be cached
   *     locally.
   * @param callback An instance of {@link UrlImageViewCallback} that is called when the image
   *     successfully finishes loading. This value can be null.
   */
  private static void setUrlDrawable(
      final Context context,
      final ImageView imageView,
      final String url,
      final int defaultResource,
      final long cacheDurationMs,
      final UrlImageViewCallback callback) {
    Drawable d = null;
    if (defaultResource != 0) {
      d = imageView.getResources().getDrawable(defaultResource);
    }
    setUrlDrawable(context, imageView, url, d, cacheDurationMs, callback);
  }

  private static boolean isNullOrEmpty(final CharSequence s) {
    return (s == null || s.equals("") || s.equals("null") || s.equals("NULL"));
  }

  private static boolean mHasCleaned = false;

  public static String getFilenameForUrl(final String url) {
    return url.hashCode() + ".urlimage";
  }

  /**
   * Clear out cached images.
   *
   * @param context
   * @param age The max age of a file. Files older than this age will be removed.
   */
  public static void cleanup(final Context context, long age) {
    if (mHasCleaned) {
      return;
    }
    mHasCleaned = true;
    try {
      // purge any *.urlimage files over a week old
      final String[] files = context.getFilesDir().list();
      if (files == null) {
        return;
      }
      for (final String file : files) {
        if (!file.endsWith(".urlimage")) {
          continue;
        }

        final File f = new File(context.getFilesDir().getAbsolutePath() + '/' + file);
        if (System.currentTimeMillis() > f.lastModified() + CACHE_DURATION_ONE_WEEK) {
          f.delete();
        }
      }
    } catch (final Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * Clear out all cached images older than a week. The same as calling cleanup(context,
   * CACHE_DURATION_ONE_WEEK);
   *
   * @param context
   */
  public static void cleanup(final Context context) {
    cleanup(context, CACHE_DURATION_ONE_WEEK);
  }

  private static boolean checkCacheDuration(File file, long cacheDurationMs) {
    return cacheDurationMs == CACHE_DURATION_INFINITE
        || System.currentTimeMillis() < file.lastModified() + cacheDurationMs;
  }

  /**
   * Download and shrink an Image located at a specified URL, and display it in the provided {@link
   * ImageView}.
   *
   * @param context A {@link Context} to allow setUrlDrawable to load and save files.
   * @param imageView The {@link ImageView} to display the image to after it is loaded.
   * @param url The URL of the image that should be loaded.
   * @param defaultDrawable A {@link Drawable} that should be displayed in {@code imageView} while
   *     the image has not been loaded. This image will also be displayed if the image fails to
   *     load. This can be set to {@code null}.
   * @param cacheDurationMs The length of time, in milliseconds, that this image should be cached
   *     locally.
   * @param callback An instance of {@link UrlImageViewCallback} that is called when the image
   *     successfully finishes loading. This value can be null.
   */
  private static void setUrlDrawable(
      final Context context,
      final ImageView imageView,
      final String url,
      final Drawable defaultDrawable,
      final long cacheDurationMs,
      final UrlImageViewCallback callback) {
    cleanup(context);
    // disassociate this ImageView from any pending downloads
    if (isNullOrEmpty(url)) {
      if (imageView != null) {
        imageView.setImageDrawable(defaultDrawable);
      }
      return;
    }

    final int tw;
    final int th;
    if (mMetrics == null) prepareResources(context);
    tw = mMetrics.widthPixels;
    th = mMetrics.heightPixels;

    final String filename = context.getFileStreamPath(getFilenameForUrl(url)).getAbsolutePath();
    final File file = new File(filename);

    if (mDeadCache == null) {
      mDeadCache = new UrlLruCache(getHeapSize(context) / 8);
    }
    Drawable drawable;
    final BitmapDrawable bd = mDeadCache.remove(url);
    if (bd != null) {
      // this drawable was resurrected, it should not be in the live cache
      clog("zombie load: " + url);
      Assert.assertTrue(url, !mAllCache.contains(bd));
      drawable = new ZombieDrawable(url, bd);
    } else {
      drawable = mLiveCache.get(url);
    }

    if (drawable != null) {
      clog("Cache hit on: " + url);
      // if the file age is older than the cache duration, force a refresh.
      // note that the file must exist, otherwise it is using a default.
      // not checking for file existence would do a network call on every
      // 404 or failed load.
      if (file.exists() && !checkCacheDuration(file, cacheDurationMs)) {
        clog("Cache hit, but file is stale. Forcing reload: " + url);
        if (drawable instanceof ZombieDrawable) ((ZombieDrawable) drawable).headshot();
        drawable = null;
      } else {
        clog("Using cached: " + url);
      }
    }

    if (drawable != null) {
      if (imageView != null) {
        imageView.setImageDrawable(drawable);
      }
      if (callback != null) {
        callback.onLoaded(imageView, drawable, url, true);
      }
      return;
    }

    // oh noes, at this point we definitely do not have the file available in memory
    // let's prepare for an asynchronous load of the image.

    // null it while it is downloading
    if (imageView != null) {
      imageView.setImageDrawable(defaultDrawable);
    }

    // since listviews reuse their views, we need to
    // take note of which url this view is waiting for.
    // This may change rapidly as the list scrolls or is filtered, etc.
    clog("Waiting for " + url);
    if (imageView != null) {
      mPendingViews.put(imageView, url);
    }

    final ArrayList<ImageView> currentDownload = mPendingDownloads.get(url);
    if (currentDownload != null) {
      // Also, multiple vies may be waiting for this url.
      // So, let's maintain a list of these views.
      // When the url is downloaded, it sets the imagedrawable for
      // every view in the list. It needs to also validate that
      // the imageview is still waiting for this url.
      if (imageView != null) {
        currentDownload.add(imageView);
      }
      return;
    }

    final ArrayList<ImageView> downloads = new ArrayList<ImageView>();
    if (imageView != null) {
      downloads.add(imageView);
    }
    mPendingDownloads.put(url, downloads);

    final int targetWidth = tw <= 0 ? Integer.MAX_VALUE : tw;
    final int targetHeight = th <= 0 ? Integer.MAX_VALUE : th;
    final Loader loader =
        new Loader() {
          @Override
          public void run() {
            try {
              result = loadDrawableFromStream(context, url, filename, targetWidth, targetHeight);
            } catch (final Exception ex) {
            }
          }
        };

    final Runnable completion =
        new Runnable() {
          @Override
          public void run() {
            Assert.assertEquals(Looper.myLooper(), Looper.getMainLooper());
            Drawable usableResult = loader.result;
            if (usableResult == null) {
              usableResult = defaultDrawable;
            }
            mPendingDownloads.remove(url);
            mLiveCache.put(url, usableResult);
            if (callback != null && imageView == null)
              callback.onLoaded(null, loader.result, url, false);
            int waitingCount = 0;
            for (final ImageView iv : downloads) {
              // validate the url it is waiting for
              final String pendingUrl = mPendingViews.get(iv);
              if (!url.equals(pendingUrl)) {
                clog("Ignoring out of date request to update view for " + url);
                continue;
              }
              waitingCount++;
              mPendingViews.remove(iv);
              if (usableResult != null) {
                //                        System.out.println(String.format("imageView: %dx%d,
                // %dx%d", imageView.getMeasuredWidth(), imageView.getMeasuredHeight(),
                // imageView.getWidth(), imageView.getHeight()));
                iv.setImageDrawable(usableResult);
                //                        System.out.println(String.format("imageView: %dx%d,
                // %dx%d", imageView.getMeasuredWidth(), imageView.getMeasuredHeight(),
                // imageView.getWidth(), imageView.getHeight()));
                if (callback != null && iv == imageView)
                  callback.onLoaded(iv, loader.result, url, false);
              }
            }
            clog("Populated: " + waitingCount);
          }
        };

    if (file.exists()) {
      try {
        if (checkCacheDuration(file, cacheDurationMs)) {
          clog(
              "File Cache hit on: "
                  + url
                  + ". "
                  + (System.currentTimeMillis() - file.lastModified())
                  + "ms old.");

          final AsyncTask<Void, Void, Void> fileloader =
              new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(final Void... params) {
                  loader.run();
                  return null;
                }

                @Override
                protected void onPostExecute(final Void result) {
                  completion.run();
                }
              };
          executeTask(fileloader);
          return;
        } else {
          clog("File cache has expired. Refreshing.");
        }
      } catch (final Exception ex) {
      }
    }

    mDownloader.download(context, url, filename, loader, completion);
  }

  private abstract static class Loader implements Runnable {
    Drawable result;
  }

  public static interface UrlDownloader {
    public void download(
        Context context, String url, String filename, Runnable loader, Runnable completion);
  }

  private static UrlDownloader mDefaultDownloader =
      new UrlDownloader() {
        @Override
        public void download(
            final Context context,
            final String url,
            final String filename,
            final Runnable loader,
            final Runnable completion) {
          final AsyncTask<Void, Void, Void> downloader =
              new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(final Void... params) {
                  try {
                    InputStream is = null;
                    if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
                      final ContentResolver cr = context.getContentResolver();
                      if (url.startsWith(ContactsContract.Contacts.CONTENT_URI.toString())) {
                        is =
                            ContactsContract.Contacts.openContactPhotoInputStream(
                                cr, Uri.parse(url));
                      } else {
                        is = cr.openInputStream(Uri.parse(url));
                      }
                    } else {
                      String thisUrl = url;
                      HttpURLConnection urlConnection;
                      while (true) {
                        final URL u = new URL(thisUrl);
                        urlConnection = (HttpURLConnection) u.openConnection();
                        urlConnection.setInstanceFollowRedirects(true);

                        if (mRequestPropertiesCallback != null) {
                          final ArrayList<NameValuePair> props =
                              mRequestPropertiesCallback.getHeadersForRequest(context, url);
                          if (props != null) {
                            for (final NameValuePair pair : props) {
                              urlConnection.addRequestProperty(pair.getName(), pair.getValue());
                            }
                          }
                        }

                        if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_MOVED_TEMP
                            && urlConnection.getResponseCode() != HttpURLConnection.HTTP_MOVED_PERM)
                          break;
                        thisUrl = urlConnection.getHeaderField("Location");
                      }

                      if (urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                        clog("Response Code: " + urlConnection.getResponseCode());
                        return null;
                      }
                      is = urlConnection.getInputStream();
                    }

                    if (is != null) {
                      final FileOutputStream fos = new FileOutputStream(filename);
                      copyStream(is, fos);
                      fos.close();
                      is.close();
                    }
                    loader.run();
                    return null;
                  } catch (final Throwable e) {
                    e.printStackTrace();
                    return null;
                  }
                }

                @Override
                protected void onPostExecute(final Void result) {
                  completion.run();
                }
              };

          executeTask(downloader);
        }
      };

  public static interface RequestPropertiesCallback {
    public ArrayList<NameValuePair> getHeadersForRequest(Context context, String url);
  }

  private static RequestPropertiesCallback mRequestPropertiesCallback;

  public static RequestPropertiesCallback getRequestPropertiesCallback() {
    return mRequestPropertiesCallback;
  }

  public static void setRequestPropertiesCallback(final RequestPropertiesCallback callback) {
    mRequestPropertiesCallback = callback;
  }

  public static void useDownloader(final UrlDownloader downloader) {
    mDownloader = downloader;
  }

  public static void useDefaultDownloader() {
    mDownloader = mDefaultDownloader;
  }

  public static UrlDownloader getDefaultDownloader() {
    return mDownloader;
  }

  private static UrlImageCache mLiveCache = UrlImageCache.getInstance();

  private static UrlLruCache mDeadCache;
  private static HashSet<BitmapDrawable> mAllCache = new HashSet<BitmapDrawable>();

  private static int getHeapSize(final Context context) {
    return ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass()
        * 1024
        * 1024;
  }

  public static class ZombieDrawable extends WrapperDrawable {
    public ZombieDrawable(final String url, final BitmapDrawable drawable) {
      super(drawable);
      mUrl = url;

      mAllCache.add(drawable);
      mDeadCache.remove(url);
      mLiveCache.put(url, this);
    }

    String mUrl;

    @Override
    protected void finalize() throws Throwable {
      super.finalize();

      if (!mHeadshot) mDeadCache.put(mUrl, mDrawable);
      mAllCache.remove(mDrawable);
      mLiveCache.remove(mUrl);
      clog("Zombie GC event " + mUrl);
    }

    // kill this zombie, forever.
    private boolean mHeadshot = false;

    public void headshot() {
      clog("BOOM! Headshot: " + mUrl);
      mHeadshot = true;
      mLiveCache.remove(mUrl);
      mAllCache.remove(mDrawable);
    }
  }

  private static UrlDownloader mDownloader = mDefaultDownloader;

  public static void executeTask(final AsyncTask<Void, Void, Void> task) {
    if (Build.VERSION.SDK_INT < Constants.HONEYCOMB) {
      task.execute();
    } else {
      executeTaskHoneycomb(task);
    }
  }

  @TargetApi(Constants.HONEYCOMB)
  private static void executeTaskHoneycomb(final AsyncTask<Void, Void, Void> task) {
    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
  }

  private static Hashtable<ImageView, String> mPendingViews = new Hashtable<ImageView, String>();
  private static Hashtable<String, ArrayList<ImageView>> mPendingDownloads =
      new Hashtable<String, ArrayList<ImageView>>();
}