Project: ohmagePhone
/*-
 * 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 edu.ucla.cens.systemlog.Analytics; 
import edu.ucla.cens.systemlog.Log; 
 
import org.ohmage.OhmageApi; 
import org.ohmage.OhmageApplication; 
 
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 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, nullnullnull, 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, nullnullnull, 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, nullnullnull, 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), nullnull
                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, nullnull, 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(); 
        } 
    } 
}