package quickutils.core.cache.image; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Message; import android.os.SystemClock; import android.widget.ImageView; import java.io.BufferedInputStream; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import quickutils.core.QuickUtils; /** * Realizes an background image loader backed by a two-level FIFO cache. If the image to be loaded * is present in the cache, it is set immediately on the given view. Otherwise, a thread from a * thread pool will be used to download the image in the background and set the image on the view as * soon as it completes. * * @author Matthias Kaeppler */ public class ImageLoader implements Runnable { public static final int HANDLER_MESSAGE_ID = 0; public static final String BITMAP_EXTRA = "quickutils:extra_bitmap"; public static final String IMAGE_URL_EXTRA = "quickutils:extra_image_url"; // the default thread pool size private static final int DEFAULT_POOL_SIZE = 3; // expire images after a day // TODO: this currently only affects the in-memory cache, so it's quite pointless private static final int DEFAULT_TTL_MINUTES = 24 * 60; private static final int DEFAULT_RETRY_HANDLER_SLEEP_TIME = 1000; private static final int DEFAULT_NUM_RETRIES = 3; private static ThreadPoolExecutor executor; private static ImageCache imageCache; private static int numRetries = DEFAULT_NUM_RETRIES; private static long expirationInMinutes = DEFAULT_TTL_MINUTES; /** * @param numThreads the maximum number of threads that will be started to download images in parallel */ public static void setThreadPoolSize(int numThreads) { executor.setMaximumPoolSize(numThreads); } /** * @param numAttempts how often the image loader should retry the image download if network connection * fails */ public static void setMaxDownloadAttempts(int numAttempts) { ImageLoader.numRetries = numAttempts; } /** * This method must be called before any other method is invoked on this class. Please note that * when using ImageLoader as part of {@link WebImageView} or {@link WebGalleryAdapter}, then * there is no need to call this method, since those classes will already do that for you. This * method is idempotent. You may call it multiple times without any side effects. * * @param context the current context */ public static synchronized void initialize(Context context) { if (executor == null) { executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(DEFAULT_POOL_SIZE); } if (imageCache == null) { imageCache = new ImageCache(25, expirationInMinutes, DEFAULT_POOL_SIZE); imageCache.enableDiskCache(context, ImageCache.DISK_CACHE_SDCARD); } } public static synchronized void initialize(Context context, long expirationInMinutes) { ImageLoader.expirationInMinutes = expirationInMinutes; initialize(context); } private String imageUrl; private ImageLoaderHandler handler; protected ImageLoader(String imageUrl, ImageLoaderHandler handler) { this.imageUrl = imageUrl; this.handler = handler; } /** * Triggers the image loader for the given image and view. The image loading will be performed * concurrently to the UI main thread, using a fixed size thread pool. The loaded image will be * posted back to the given ImageView upon completion. * * @param imageUrl the URL of the image to download * @param imageView the ImageView which should be updated with the new image */ public static void load(String imageUrl, ImageView imageView) { load(imageUrl, imageView, new ImageLoaderHandler(imageView, imageUrl), null, null); } /** * Triggers the image loader for the given image and view and sets a dummy image while waiting * for the download to finish. The image loading will be performed concurrently to the UI main * thread, using a fixed size thread pool. The loaded image will be posted back to the given * ImageView upon completion. * * @param imageUrl the URL of the image to download * @param imageView the ImageView which should be updated with the new image * @param dummyDrawable the Drawable set to the ImageView while waiting for the image to be downloaded * @param errorDrawable the Drawable set to the ImageView if a download error occurs */ public static void load(String imageUrl, ImageView imageView, Drawable dummyDrawable, Drawable errorDrawable) { load(imageUrl, imageView, new ImageLoaderHandler(imageView, imageUrl, errorDrawable), dummyDrawable, errorDrawable); } /** * Triggers the image loader for the given image and view and sets a dummy image while waiting * for the download to finish. The image loading will be performed concurrently to the UI main * thread, using a fixed size thread pool. The loaded image will be posted back to the given * ImageView upon completion. * * @param imageUrl the URL of the image to download * @param imageView the ImageView which should be updated with the new image * @param dummyDrawable the Drawable set to the ImageView while waiting for the image to be downloaded * @param errorDrawable the Drawable set to the ImageView if a download error occurs */ public static void load(String imageUrl, ImageView imageView, int dummyDrawable, int errorDrawable) { Drawable dummy = QuickUtils.getContext().getResources().getDrawable(dummyDrawable); Drawable error = QuickUtils.getContext().getResources().getDrawable(errorDrawable); load(imageUrl, imageView, new ImageLoaderHandler(imageView, imageUrl, error), dummy, error); } /** * Triggers the image loader for the given image and handler. The image loading will be * performed concurrently to the UI main thread, using a fixed size thread pool. The loaded * image will not be automatically posted to an ImageView; instead, you can pass a custom * {@link ImageLoaderHandler} and handle the loaded image yourself (e.g. cache it for later * use). * * @param imageUrl the URL of the image to download * @param handler the handler which is used to handle the downloaded image */ public static void load(String imageUrl, ImageLoaderHandler handler) { load(imageUrl, handler.getImageView(), handler, null, null); } /** * Triggers the image loader for the given image and handler. The image loading will be * performed concurrently to the UI main thread, using a fixed size thread pool. The loaded * image will not be automatically posted to an ImageView; instead, you can pass a custom * {@link ImageLoaderHandler} and handle the loaded image yourself (e.g. cache it for later * use). * * @param imageUrl the URL of the image to download * @param handler the handler which is used to handle the downloaded image * @param dummyDrawable the Drawable set to the ImageView while waiting for the image to be downloaded * @param errorDrawable the Drawable set to the ImageView if a download error occurs */ public static void load(String imageUrl, ImageLoaderHandler handler, Drawable dummyDrawable, Drawable errorDrawable) { load(imageUrl, handler.getImageView(), handler, dummyDrawable, errorDrawable); } private static void load(String imageUrl, ImageView imageView, ImageLoaderHandler handler, Drawable dummyDrawable, Drawable errorDrawable) { if (imageView != null) { if (imageUrl == null) { // In a ListView views are reused, so we must be sure to remove the tag that could // have been set to the ImageView to prevent that the wrong image is set. imageView.setTag(null); imageView.setImageDrawable(dummyDrawable); return; } String oldImageUrl = (String) imageView.getTag(); if (imageUrl.equals(oldImageUrl)) { // nothing to do return; } else { // Set the dummy image while waiting for the actual image to be downloaded. imageView.setImageDrawable(dummyDrawable); imageView.setTag(imageUrl); } } if (imageCache.containsKeyInMemory(imageUrl)) { // do not go through message passing, handle directly instead handler.handleImageLoaded(imageCache.getBitmap(imageUrl), null); } else { executor.execute(new ImageLoader(imageUrl, handler)); } } /** * Clears the 1st-level cache (in-memory cache). A good candidate for calling in * {@link android.app.Application#onLowMemory()}. */ public static void clearCache() { imageCache.clear(); } /** * Returns the image cache backing this image loader. * * @return the {@link ImageCache} */ public static ImageCache getImageCache() { return imageCache; } /** * The job method run on a worker thread. It will first query the image cache, and on a miss, * download the image from the Web. */ public void run() { // TODO: if we had a way to check for in-memory hits, we could improve performance by // fetching an image from the in-memory cache on the main thread Bitmap bitmap = imageCache.getBitmap(imageUrl); if (bitmap == null) { bitmap = downloadImage(); } // TODO: gracefully handle this case. notifyImageLoaded(imageUrl, bitmap); } // TODO: we could probably improve performance by re-using connections instead of closing them // after each and every download protected Bitmap downloadImage() { int timesTried = 1; while (timesTried <= numRetries) { try { byte[] imageData = retrieveImageData(); if (imageData != null) { imageCache.put(imageUrl, imageData); } else { break; } return BitmapFactory.decodeByteArray(imageData, 0, imageData.length); } catch (Throwable e) { QuickUtils.log.w("download for " + imageUrl + " failed (attempt " + timesTried + ")"); e.printStackTrace(); SystemClock.sleep(DEFAULT_RETRY_HANDLER_SLEEP_TIME); timesTried++; } } return null; } protected byte[] retrieveImageData() throws IOException { URL url = new URL(imageUrl); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // determine the image size and allocate a buffer int fileSize = connection.getContentLength(); if (fileSize < 0) { return null; } byte[] imageData = new byte[fileSize]; // download the file BufferedInputStream istream = new BufferedInputStream(connection.getInputStream()); int bytesRead = 0; int offset = 0; while (bytesRead != -1 && offset < fileSize) { bytesRead = istream.read(imageData, offset, fileSize - offset); offset += bytesRead; } // clean up istream.close(); connection.disconnect(); return imageData; } public void notifyImageLoaded(String url, Bitmap bitmap) { Message message = new Message(); message.what = HANDLER_MESSAGE_ID; Bundle data = new Bundle(); data.putString(IMAGE_URL_EXTRA, url); Bitmap image = bitmap; data.putParcelable(BITMAP_EXTRA, image); message.setData(data); handler.sendMessage(message); } }