com.google.android.imageloader.ImageLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.google.android.imageloader.ImageLoader.java

Source

/*-
 * Copyright (C) 2010 Google Inc.
 *
 * 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.google.android.imageloader;

import android.app.Activity;
import android.app.Application;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import android.support.v4.content.ModernAsyncTask;
import android.text.TextUtils;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;

import org.ohmage.OhmageApi;
import org.ohmage.OhmageApplication;
import org.ohmage.logprobe.Analytics;
import org.ohmage.logprobe.Log;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.ContentHandler;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.net.URLStreamHandlerFactory;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * A helper class to load images asynchronously.
 */
public final class ImageLoader {

    private static final String TAG = "ImageLoader";

    /**
     * The default maximum number of active tasks.
     */
    public static final int DEFAULT_TASK_LIMIT = 3;

    /**
     * The default cache size (in bytes).
     */
    // 25% of available memory, up to a maximum of 16MB
    public static final long DEFAULT_CACHE_SIZE = Math.min(Runtime.getRuntime().maxMemory() / 4, 16 * 1024 * 1024);

    /**
     * Use with {@link Context#getSystemService(String)} to retrieve an
     * {@link ImageLoader} for loading images.
     * <p>
     * Since {@link ImageLoader} is not a standard system service, you must
     * create a custom {@link Application} subclass implementing
     * {@link Application#getSystemService(String)} and add it to your
     * {@code AndroidManifest.xml}.
     * <p>
     * Using this constant is optional and it is only provided for convenience
     * and to promote consistency across deployments of this component.
     */
    public static final String IMAGE_LOADER_SERVICE = "com.google.android.imageloader";

    /**
     * Gets the {@link ImageLoader} from a {@link Context}.
     *
     * @throws IllegalStateException if the {@link Application} does not have an
     *             {@link ImageLoader}.
     * @see #IMAGE_LOADER_SERVICE
     */
    public static ImageLoader get(Context context) {
        ImageLoader loader = (ImageLoader) context.getSystemService(IMAGE_LOADER_SERVICE);
        if (loader == null) {
            context = context.getApplicationContext();
            loader = (ImageLoader) context.getSystemService(IMAGE_LOADER_SERVICE);
        }
        if (loader == null) {
            throw new IllegalStateException("ImageLoader not available");
        }
        return loader;
    }

    /**
     * Callback interface for load and error events.
     * <p>
     * This interface is only applicable when binding a stand-alone
     * {@link ImageView}. When the target {@link ImageView} is in an
     * {@link AdapterView},
     * {@link ImageLoader#bind(BaseAdapter, ImageView, String)} will be called
     * implicitly by {@link BaseAdapter#notifyDataSetChanged()}.
     */
    public interface Callback {
        /**
         * Notifies an observer that an image was loaded.
         * <p>
         * The bitmap will be assigned to the {@link ImageView} automatically.
         * <p>
         * Use this callback to dismiss any loading indicators.
         *
         * @param view the {@link ImageView} that was loaded.
         * @param url the URL that was loaded.
         */
        void onImageLoaded(ImageView view, String url);

        /**
         * Notifies an observer that an image could not be loaded.
         *
         * @param view the {@link ImageView} that could not be loaded.
         * @param url the URL that could not be loaded.
         * @param error the exception that was thrown.
         */
        void onImageError(ImageView view, String url, Throwable error);
    }

    public static enum BindResult {
        /**
         * Returned when an image is bound to an {@link ImageView} immediately
         * because it was already loaded.
         */
        OK,
        /**
         * Returned when an image needs to be loaded asynchronously.
         * <p>
         * Callers may wish to assign a placeholder or show a progress spinner
         * while the image is being loaded whenever this value is returned.
         */
        LOADING,
        /**
         * Returned when an attempt to load the image has already been made and
         * it failed.
         * <p>
         * Callers may wish to show an error indicator when this value is
         * returned.
         *
         * @see ImageLoader.Callback
         */
        ERROR
    }

    private static String getProtocol(String url) {
        Uri uri = Uri.parse(url);
        return uri.getScheme();
    }

    private final ContentHandler mBitmapContentHandler;

    private final ContentHandler mPrefetchContentHandler;

    private final URLStreamHandlerFactory mURLStreamHandlerFactory;

    private final HashMap<String, URLStreamHandler> mStreamHandlers;

    private final LinkedList<ImageRequest> mRequests;

    /**
     * A cache containing recently used bitmaps.
     * <p>
     * Use soft references so that the application does not run out of memory in
     * the case where one or more of the bitmaps are large.
     */
    private final Map<String, Bitmap> mBitmaps;

    /**
     * Recent errors encountered when loading bitmaps.
     */
    private final Map<String, ImageError> mErrors;

    /**
     * Tracks the last URL that was bound to an {@link ImageView}.
     * <p>
     * This ensures that the right image is shown in the case where a new URL is
     * assigned to an {@link ImageView} before the previous asynchronous task
     * completes.
     * <p>
     * This <em>does not</em> ensure that an image assigned with
     * {@link ImageView#setImageBitmap(Bitmap)},
     * {@link ImageView#setImageDrawable(android.graphics.drawable.Drawable)},
     * {@link ImageView#setImageResource(int)}, or
     * {@link ImageView#setImageURI(android.net.Uri)} is not replaced. This
     * behavior is important because callers may invoke these methods to assign
     * a placeholder when a bind method returns {@link BindResult#LOADING} or
     * {@link BindResult#ERROR}.
     */
    private final Map<ImageView, String> mImageViewBinding;

    /**
     * The maximum number of active tasks.
     */
    private final int mMaxTaskCount;

    /**
     * The current number of active tasks.
     */
    private int mActiveTaskCount;

    /**
     * Creates an {@link ImageLoader}.
     *
     * @param taskLimit the maximum number of background tasks that may be
     *            active at one time.
     * @param streamFactory a {@link URLStreamHandlerFactory} for creating
     *            connections to special URLs such as {@code content://} URIs.
     *            This parameter can be {@code null} if the {@link ImageLoader}
     *            only needs to load images over HTTP or if a custom
     *            {@link URLStreamHandlerFactory} has already been passed to
     *            {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)}
     * @param bitmapHandler a {@link ContentHandler} for loading images.
     *            {@link ContentHandler#getContent(URLConnection)} must either
     *            return a {@link Bitmap} or throw an {@link IOException}. This
     *            parameter can be {@code null} to use the default
     *            {@link BitmapContentHandler}.
     * @param prefetchHandler a {@link ContentHandler} for caching a remote URL
     *            as a file, without parsing it or loading it into memory.
     *            {@link ContentHandler#getContent(URLConnection)} should always
     *            return {@code null}. If the URL passed to the
     *            {@link ContentHandler} is already local (for example,
     *            {@code file://}), this {@link ContentHandler} should do
     *            nothing. The {@link ContentHandler} can be {@code null} if
     *            pre-fetching is not required.
     * @param cacheSize the maximum size of the image cache (in bytes).
     * @param handler a {@link Handler} identifying the callback thread, or
     *            {@code} null for the main thread.
     * @throws NullPointerException if the factory is {@code null}.
     */
    public ImageLoader(int taskLimit, URLStreamHandlerFactory streamFactory, ContentHandler bitmapHandler,
            ContentHandler prefetchHandler, long cacheSize, Handler handler) {
        if (taskLimit < 1) {
            throw new IllegalArgumentException("Task limit must be positive");
        }
        if (cacheSize < 1) {
            throw new IllegalArgumentException("Cache size must be positive");
        }
        mMaxTaskCount = taskLimit;
        mURLStreamHandlerFactory = streamFactory;
        mStreamHandlers = streamFactory != null ? new HashMap<String, URLStreamHandler>() : null;
        mBitmapContentHandler = bitmapHandler != null ? bitmapHandler : new BitmapContentHandler();
        mPrefetchContentHandler = prefetchHandler;

        mImageViewBinding = new WeakHashMap<ImageView, String>();

        mRequests = new LinkedList<ImageRequest>();

        // Use a LruCache to prevent the set of keys from growing too large.
        // The Maps must be synchronized because they are accessed
        // by the UI thread and by background threads.
        mBitmaps = Collections.synchronizedMap(new BitmapCache<String>(cacheSize));
        mErrors = Collections.synchronizedMap(new LruCache<String, ImageError>());
    }

    /**
     * Creates a basic {@link ImageLoader} with support for HTTP URLs and
     * in-memory caching.
     * <p>
     * Persistent caching and content:// URIs are not supported when this
     * constructor is used.
     */
    public ImageLoader() {
        this(DEFAULT_TASK_LIMIT, null, null, null, DEFAULT_CACHE_SIZE, null);
    }

    /**
     * Creates a basic {@link ImageLoader} with support for HTTP URLs and
     * in-memory caching.
     * <p>
     * Persistent caching and content:// URIs are not supported when this
     * constructor is used.
     *
     * @param taskLimit the maximum number of background tasks that may be
     *            active at a time.
     */
    public ImageLoader(int taskLimit) {
        this(taskLimit, null, null, null, DEFAULT_CACHE_SIZE, null);
    }

    /**
     * Creates a basic {@link ImageLoader} with support for HTTP URLs and
     * in-memory caching.
     * <p>
     * Persistent caching and content:// URIs are not supported when this
     * constructor is used.
     *
     * @param cacheSize the maximum size of the image cache (in bytes).
     */
    public ImageLoader(long cacheSize) {
        this(DEFAULT_TASK_LIMIT, null, null, null, cacheSize, null);
    }

    /**
     * Creates an {@link ImageLoader} with support for pre-fetching.
     *
     * @param bitmapHandler a {@link ContentHandler} that reads, caches, and
     *            returns a {@link Bitmap}.
     * @param prefetchHandler a {@link ContentHandler} for caching a remote URL
     *            as a file, without parsing it or loading it into memory.
     *            {@link ContentHandler#getContent(URLConnection)} should always
     *            return {@code null}. If the URL passed to the
     *            {@link ContentHandler} is already local (for example,
     *            {@code file://}), this {@link ContentHandler} should return
     *            {@code null} immediately.
     */
    public ImageLoader(ContentHandler bitmapHandler, ContentHandler prefetchHandler) {
        this(DEFAULT_TASK_LIMIT, null, bitmapHandler, prefetchHandler, DEFAULT_CACHE_SIZE, null);
    }

    /**
     * Creates an {@link ImageLoader} with support for http:// and content://
     * URIs.
     * <p>
     * Prefetching is not supported when this constructor is used.
     *
     * @param resolver a {@link ContentResolver} for accessing content:// URIs.
     */
    public ImageLoader(ContentResolver resolver) {
        this(DEFAULT_TASK_LIMIT, new ContentURLStreamHandlerFactory(resolver), null, null, DEFAULT_CACHE_SIZE,
                null);
    }

    /**
     * Creates an {@link ImageLoader} with a custom
     * {@link URLStreamHandlerFactory}.
     * <p>
     * Use this constructor when loading images with protocols other than
     * {@code http://} and when a custom {@link URLStreamHandlerFactory} has not
     * already been installed with
     * {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)}. If the
     * only additional protocol support required is for {@code content://} URIs,
     * consider using {@link #ImageLoader(ContentResolver)}.
     * <p>
     * Prefetching is not supported when this constructor is used.
     */
    public ImageLoader(URLStreamHandlerFactory factory) {
        this(DEFAULT_TASK_LIMIT, factory, null, null, DEFAULT_CACHE_SIZE, null);
    }

    private URLStreamHandler getURLStreamHandler(String protocol) {
        URLStreamHandlerFactory factory = mURLStreamHandlerFactory;
        if (factory == null) {
            return null;
        }
        HashMap<String, URLStreamHandler> handlers = mStreamHandlers;
        synchronized (handlers) {
            URLStreamHandler handler = handlers.get(protocol);
            if (handler == null) {
                handler = factory.createURLStreamHandler(protocol);
                if (handler != null) {
                    handlers.put(protocol, handler);
                }
            }
            return handler;
        }
    }

    /**
     * Creates tasks to service any pending requests until {@link #mRequests} is
     * empty or {@link #mMaxTaskCount} is reached.
     */
    void flushRequests() {
        while (mActiveTaskCount < mMaxTaskCount && !mRequests.isEmpty()) {
            new ImageTask().executeOnThreadPool(mRequests.poll());
        }
    }

    private void enqueueRequest(ImageRequest request) {
        mRequests.add(request);
        flushRequests();
    }

    private void insertRequestAtFrontOfQueue(ImageRequest request) {
        mRequests.add(0, request);
        flushRequests();
    }

    /**
     * Binds a URL to an {@link ImageView} within an {@link android.widget.AdapterView}.
     *
     * @param adapter the adapter for the {@link android.widget.AdapterView}.
     * @param view the {@link ImageView}.
     * @param url the image URL.
     * @return a {@link BindResult}.
     * @throws NullPointerException if any of the arguments are {@code null}.
     */
    public BindResult bind(BaseAdapter adapter, ImageView view, String url) {
        if (adapter == null) {
            throw new NullPointerException("Adapter is null");
        }
        if (view == null) {
            throw new NullPointerException("ImageView is null");
        }
        if (url == null) {
            throw new NullPointerException("URL is null");
        }
        Bitmap bitmap = getBitmap(url);
        ImageError error = getError(url);
        if (bitmap != null) {
            view.setImageBitmap(bitmap);
            return BindResult.OK;
        } else {
            // Clear the ImageView by default.
            // The caller can set their own placeholder
            // based on the return value.
            view.setImageDrawable(null);

            if (error != null) {
                return BindResult.ERROR;
            } else {
                ImageRequest request = new ImageRequest(adapter, url);

                // For adapters, post the latest requests
                // at the front of the queue in case the user
                // has already scrolled past most of the images
                // that are currently in the queue.
                insertRequestAtFrontOfQueue(request);

                return BindResult.LOADING;
            }
        }
    }

    /**
     * Binds a URL to an {@link ImageView} within an {@link android.widget.ExpandableListView}.
     *
     * @param adapter the adapter for the {@link android.widget.ExpandableListView}.
     * @param view the {@link ImageView}.
     * @param url the image URL.
     * @return a {@link BindResult}.
     * @throws NullPointerException if any of the arguments are {@code null}.
     */
    public BindResult bind(BaseExpandableListAdapter adapter, ImageView view, String url) {
        if (adapter == null) {
            throw new NullPointerException("Adapter is null");
        }
        if (view == null) {
            throw new NullPointerException("ImageView is null");
        }
        if (url == null) {
            throw new NullPointerException("URL is null");
        }
        Bitmap bitmap = getBitmap(url);
        ImageError error = getError(url);
        if (bitmap != null) {
            view.setImageBitmap(bitmap);
            return BindResult.OK;
        } else {
            // Clear the ImageView by default.
            // The caller can set their own placeholder
            // based on the return value.
            view.setImageDrawable(null);

            if (error != null) {
                return BindResult.ERROR;
            } else {
                ImageRequest request = new ImageRequest(adapter, url);

                // For adapters, post the latest requests
                // at the front of the queue in case the user
                // has already scrolled past most of the images
                // that are currently in the queue.
                insertRequestAtFrontOfQueue(request);

                return BindResult.LOADING;
            }
        }
    }

    /**
     * Binds an image at the given URL to an {@link ImageView}.
     * <p>
     * If the image needs to be loaded asynchronously, it will be assigned at a
     * later time, replacing any existing {@link Drawable} unless
     * {@link #unbind(ImageView)} is called or
     * {@link #bind(ImageView, String, Callback)} is called with the same
     * {@link ImageView}, but a different URL.
     * <p>
     * Use {@link #bind(BaseAdapter, ImageView, String)} instead of this method
     * when the {@link ImageView} is in an {@link android.widget.AdapterView} so
     * that the image will be bound correctly in the case where it has been
     * assigned to a different position since the asynchronous request was
     * started.
     *
     * @param view the {@link ImageView} to bind.
     * @param url the image URL.s
     * @param callback invoked after the image has finished loading or after an
     *            error. The callback may be executed before this method returns
     *            when the result is cached. This parameter can be {@code null}
     *            if a callback is not required.
     * @return a {@link BindResult}.
     * @throws NullPointerException if a required argument is {@code null}
     */
    public BindResult bind(ImageView view, String url, Callback callback) {
        if (view == null) {
            throw new NullPointerException("ImageView is null");
        }
        if (url == null) {
            throw new NullPointerException("URL is null");
        }
        mImageViewBinding.put(view, url);
        Bitmap bitmap = getBitmap(url);
        ImageError error = getError(url);
        if (bitmap != null) {
            view.setImageBitmap(bitmap);
            if (callback != null) {
                callback.onImageLoaded(view, url);
            }
            return BindResult.OK;
        } else {
            // Clear the ImageView by default.
            // The caller can set their own placeholder
            // based on the return value.
            view.setImageDrawable(null);

            if (error != null) {
                if (callback != null) {
                    callback.onImageError(view, url, error.getCause());
                }
                return BindResult.ERROR;
            } else {
                ImageRequest request = new ImageRequest(view, url, callback);
                enqueueRequest(request);
                return BindResult.LOADING;
            }
        }
    }

    /**
     * Cancels an asynchronous request to bind an image URL to an
     * {@link ImageView} and clears the {@link ImageView}.
     *
     * @see #bind(ImageView, String, Callback)
     */
    public void unbind(ImageView view) {
        mImageViewBinding.remove(view);
        view.setImageDrawable(null);
    }

    /**
     * Clears any cached errors.
     * <p>
     * Call this method when a network connection is restored, or the user
     * invokes a manual refresh of the screen.
     */
    public void clearErrors() {
        mErrors.clear();
    }

    /**
     * Pre-loads an image into memory.
     * <p>
     * The image may be unloaded if memory is low. Use {@link #prefetch(String)}
     * and a file-based cache to pre-load more images.
     *
     * @param url the image URL
     * @throws NullPointerException if the URL is {@code null}
     */
    public void preload(String url) {
        if (url == null) {
            throw new NullPointerException();
        }
        if (null != getBitmap(url)) {
            // The image is already loaded
            return;
        }
        if (null != getError(url)) {
            // A recent attempt to load the image failed,
            // therefore this attempt is likely to fail as well.
            return;
        }
        boolean loadBitmap = true;
        ImageRequest task = new ImageRequest(url, loadBitmap);
        enqueueRequest(task);
    }

    /**
     * Pre-loads a range of images into memory from a {@link Cursor}.
     * <p>
     * Typically, an {@link Activity} would register a {@link DataSetObserver}
     * and an {@link android.widget.AdapterView.OnItemSelectedListener}, then
     * call this method to prime the in-memory cache with images adjacent to the
     * current selection whenever the selection or data changes.
     * <p>
     * Any invalid positions in the specified range will be silently ignored.
     *
     * @param cursor a {@link Cursor} containing the image URLs.
     * @param columnIndex the column index of the image URL. The column value
     *            may be {@code NULL}.
     * @param start the first position to load. For example, {@code
     *            selectedPosition - 5}.
     * @param end the first position not to load. For example, {@code
     *            selectedPosition + 5}.
     * @see #preload(String)
     */
    public void preload(Cursor cursor, int columnIndex, int start, int end) {
        for (int position = start; position < end; position++) {
            if (cursor.moveToPosition(position)) {
                String url = cursor.getString(columnIndex);
                if (!TextUtils.isEmpty(url)) {
                    preload(url);
                }
            }
        }
    }

    /**
     * Pre-fetches the binary content for an image and stores it in a file-based
     * cache (if it is not already cached locally) without loading the image
     * data into memory.
     * <p>
     * Pre-fetching should not be used unless a {@link ContentHandler} with
     * support for persistent caching was passed to the constructor.
     *
     * @param url the URL to pre-fetch.
     * @throws NullPointerException if the URL is {@code null}
     */
    public void prefetch(String url) {
        if (url == null) {
            throw new NullPointerException();
        }
        if (null != getBitmap(url)) {
            // The image is already loaded, therefore
            // it does not need to be prefetched.
            return;
        }
        if (null != getError(url)) {
            // A recent attempt to load or prefetch the image failed,
            // therefore this attempt is likely to fail as well.
            return;
        }
        boolean loadBitmap = false;
        ImageRequest request = new ImageRequest(url, loadBitmap);
        enqueueRequest(request);
    }

    /**
     * Pre-fetches the binary content for an image and stores it in a file-based
     * cache (if it is not already cached locally) without loading the image
     * data into memory.
     * <p>
     * Pre-fetching should not be used unless a {@link ContentHandler} with
     * support for persistent caching was passed to the constructor.
     * </p>
     * <p>
     * Should not be called from the UI thread
     * </p>
     * @param url the URL to pre-fetch.
     * @throws IOException 
     * @throws MalformedURLException 
     * @throws NullPointerException if the URL is {@code null}
     */
    public void prefetchBlocking(String url) throws MalformedURLException, IOException {
        if (url == null) {
            throw new NullPointerException();
        }
        if (null != getBitmap(url)) {
            // The image is already loaded, therefore
            // it does not need to be prefetched.
            return;
        }
        if (null != getError(url)) {
            // A recent attempt to load or prefetch the image failed,
            // therefore this attempt is likely to fail as well.
            return;
        }
        boolean loadBitmap = false;
        ImageRequest request = new ImageRequest(url, loadBitmap);

        String protocol = getProtocol(url);
        URLStreamHandler streamHandler = getURLStreamHandler(protocol);
        request.loadImage(new URL(null, url, streamHandler));
    }

    /**
     * Pre-fetches the binary content for images referenced by a {@link Cursor},
     * without loading the image data into memory.
     * <p>
     * Pre-fetching should not be used unless a {@link ContentHandler} with
     * support for persistent caching was passed to the constructor.
     * <p>
     * Typically, an {@link Activity} would register a {@link DataSetObserver}
     * and call this method from {@link DataSetObserver#onChanged()} to load
     * off-screen images into a file-based cache when they are not already
     * present in the cache.
     *
     * @param cursor the {@link Cursor} containing the image URLs.
     * @param columnIndex the column index of the image URL. The column value
     *            may be {@code NULL}.
     * @see #prefetch(String)
     */
    public void prefetch(Cursor cursor, int columnIndex) {
        for (int position = 0; cursor.moveToPosition(position); position++) {
            String url = cursor.getString(columnIndex);
            if (!TextUtils.isEmpty(url)) {
                prefetch(url);
            }
        }
    }

    /**
     * Add a bitmap to the cache
     * @param url
     * @param bitmap
     */
    public void putBitmap(String url, Bitmap bitmap) {
        mBitmaps.put(url, bitmap);
    }

    private void putError(String url, ImageError error) {
        mErrors.put(url, error);
    }

    private Bitmap getBitmap(String url) {
        return mBitmaps.get(url);
    }

    private ImageError getError(String url) {
        ImageError error = mErrors.get(url);
        return error != null && !error.isExpired() ? error : null;
    }

    /**
     * Returns {@code true} if there was an error the last time the given URL
     * was accessed and the error is not expired, {@code false} otherwise.
     */
    private boolean hasError(String url) {
        return getError(url) != null;
    }

    private class ImageRequest {

        private final ImageCallback mCallback;

        private final String mUrl;

        private final boolean mLoadBitmap;

        private Bitmap mBitmap;

        private ImageError mError;

        private ImageRequest(String url, ImageCallback callback, boolean loadBitmap) {
            mUrl = url;
            mCallback = callback;
            mLoadBitmap = loadBitmap;
        }

        /**
         * Creates an {@link ImageTask} to load a {@link Bitmap} for an
         * {@link ImageView} in an {@link android.widget.AdapterView}.
         */
        public ImageRequest(BaseAdapter adapter, String url) {
            this(url, new BaseAdapterCallback(adapter), true);
        }

        /**
         * Creates an {@link ImageTask} to load a {@link Bitmap} for an
         * {@link ImageView} in an {@link android.widget.ExpandableListView}.
         */
        public ImageRequest(BaseExpandableListAdapter adapter, String url) {
            this(url, new BaseExpandableListAdapterCallback(adapter), true);
        }

        /**
         * Creates an {@link ImageTask} to load a {@link Bitmap} for an
         * {@link ImageView}.
         */
        public ImageRequest(ImageView view, String url, Callback callback) {
            this(url, new ImageViewCallback(view, callback), true);
        }

        /**
         * Creates an {@link ImageTask} to prime the cache.
         */
        public ImageRequest(String url, boolean loadBitmap) {
            this(url, null, loadBitmap);
        }

        private Bitmap loadImage(URL url) throws IOException {
            URLConnection connection = url.openConnection();
            int length = connection.getContentLength();
            Bitmap bitmap = (Bitmap) mBitmapContentHandler.getContent(connection);
            Analytics.network(OhmageApplication.getContext(), "/" + OhmageApi.IMAGE_READ_PATH, length);
            return bitmap;
        }

        /**
         * Executes the {@link ImageTask}.
         *
         * @return {@code true} if the result for this {@link ImageTask} should
         *         be posted, {@code false} otherwise.
         */
        public boolean execute() {
            try {
                if (mCallback != null) {
                    if (mCallback.unwanted()) {
                        return false;
                    }
                }
                // Check if the last attempt to load the URL had an error
                mError = getError(mUrl);
                if (mError != null) {
                    return true;
                }

                // Check if the Bitmap is already cached in memory
                mBitmap = getBitmap(mUrl);
                if (mBitmap != null) {
                    // Keep a hard reference until the view has been notified.
                    return true;
                }

                String protocol = getProtocol(mUrl);
                URLStreamHandler streamHandler = getURLStreamHandler(protocol);
                URL url = new URL(null, mUrl, streamHandler);

                if (mLoadBitmap) {
                    try {
                        mBitmap = loadImage(url);
                    } catch (OutOfMemoryError e) {
                        // The VM does not always free-up memory as it should,
                        // so manually invoke the garbage collector
                        // and try loading the image again.
                        System.gc();
                        mBitmap = loadImage(url);
                    }
                    if (mBitmap == null) {
                        throw new NullPointerException("ContentHandler returned null");
                    }
                    return true;
                } else {
                    if (mPrefetchContentHandler != null) {
                        // Cache the URL without loading a Bitmap into memory.
                        URLConnection connection = url.openConnection();
                        mPrefetchContentHandler.getContent(connection);
                    }
                    mBitmap = null;
                    return false;
                }
            } catch (IOException e) {
                mError = new ImageError(e);
                return true;
            } catch (RuntimeException e) {
                mError = new ImageError(e);
                return true;
            } catch (Error e) {
                mError = new ImageError(e);
                return true;
            }
        }

        public void publishResult() {
            if (mBitmap != null) {
                putBitmap(mUrl, mBitmap);
            } else if (mError != null && !hasError(mUrl)) {
                Log.e(TAG, "Failed to load " + mUrl, mError.getCause());
                putError(mUrl, mError);
            }
            if (mCallback != null) {
                mCallback.send(mUrl, mBitmap, mError);
            }
        }
    }

    private interface ImageCallback {
        boolean unwanted();

        void send(String url, Bitmap bitmap, ImageError error);
    }

    private final class ImageViewCallback implements ImageCallback {

        // TODO: Use WeakReferences?

        private final ImageView mImageView;
        private final Callback mCallback;

        public ImageViewCallback(ImageView imageView, Callback callback) {
            mImageView = imageView;
            mCallback = callback;
        }

        /** {@inheritDoc} */
        @Override
        public boolean unwanted() {
            // Always complete the callback
            return false;
        }

        /** {@inheritDoc} */
        @Override
        public void send(String url, Bitmap bitmap, ImageError error) {
            String binding = mImageViewBinding.get(mImageView);
            if (!TextUtils.equals(binding, url)) {
                // The ImageView has been unbound or bound to a
                // different URL since the task was started.
                return;
            }
            if (bitmap != null) {
                mImageView.setImageBitmap(bitmap);
                if (mCallback != null) {
                    mCallback.onImageLoaded(mImageView, url);
                }
            } else if (error != null) {
                if (mCallback != null) {
                    mCallback.onImageError(mImageView, url, error.getCause());
                }
            }
        }
    }

    private static final class BaseAdapterCallback implements ImageCallback {
        private final WeakReference<BaseAdapter> mAdapter;

        public BaseAdapterCallback(BaseAdapter adapter) {
            mAdapter = new WeakReference<BaseAdapter>(adapter);
        }

        /** {@inheritDoc} */
        @Override
        public boolean unwanted() {
            return mAdapter.get() == null;
        }

        /** {@inheritDoc} */
        @Override
        public void send(String url, Bitmap bitmap, ImageError error) {
            BaseAdapter adapter = mAdapter.get();
            if (adapter == null) {
                // The adapter is no longer in use
                return;
            }
            if (!adapter.isEmpty()) {
                adapter.notifyDataSetChanged();
            } else {
                // The adapter is empty or no longer in use.
                // It is important that BaseAdapter#notifyDataSetChanged()
                // is not called when the adapter is empty because this
                // may indicate that the data is valid when it is not.
                // For example: when the adapter cursor is deactivated.
            }
        }
    }

    private static final class BaseExpandableListAdapterCallback implements ImageCallback {

        private final WeakReference<BaseExpandableListAdapter> mAdapter;

        public BaseExpandableListAdapterCallback(BaseExpandableListAdapter adapter) {
            mAdapter = new WeakReference<BaseExpandableListAdapter>(adapter);
        }

        /** {@inheritDoc} */
        @Override
        public boolean unwanted() {
            return mAdapter.get() == null;
        }

        /** {@inheritDoc} */
        @Override
        public void send(String url, Bitmap bitmap, ImageError error) {
            BaseExpandableListAdapter adapter = mAdapter.get();
            if (adapter == null) {
                // The adapter is no longer in use
                return;
            }
            if (!adapter.isEmpty()) {
                adapter.notifyDataSetChanged();
            } else {
                // The adapter is empty or no longer in use.
                // It is important that BaseAdapter#notifyDataSetChanged()
                // is not called when the adapter is empty because this
                // may indicate that the data is valid when it is not.
                // For example: when the adapter cursor is deactivated.
            }
        }
    }

    private class ImageTask extends ModernAsyncTask<ImageRequest, ImageRequest, Void> {

        public final ModernAsyncTask<ImageRequest, ImageRequest, Void> executeOnThreadPool(ImageRequest... params) {
            return execute(params);
        }

        @Override
        protected void onPreExecute() {
            mActiveTaskCount++;
        }

        @Override
        protected Void doInBackground(ImageRequest... requests) {
            for (ImageRequest request : requests) {
                if (request.execute()) {
                    publishProgress(request);
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(ImageRequest... values) {
            for (ImageRequest request : values) {
                request.publishResult();
            }
        }

        @Override
        protected void onPostExecute(Void result) {
            mActiveTaskCount--;
            flushRequests();
        }
    }

    private static class ImageError {
        private static final int TIMEOUT = 2 * 60 * 1000; // Two minutes

        private final Throwable mCause;

        private final long mTimestamp;

        public ImageError(Throwable cause) {
            if (cause == null) {
                throw new NullPointerException();
            }
            mCause = cause;
            mTimestamp = now();
        }

        public boolean isExpired() {
            return (now() - mTimestamp) > TIMEOUT;
        }

        public Throwable getCause() {
            return mCause;
        }

        private static long now() {
            return SystemClock.elapsedRealtime();
        }
    }
}