/*- * 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(); } } }