/* Copyright (c) 2009 Matthias Kaeppler * * 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.cinla.imageloader; 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 com.cinla.ringtone.Constant; import com.cinla.ringtone.NetUtils; import com.cinla.ringtone.Utils; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Message; import android.os.SystemClock; import android.util.Log; import android.widget.ImageView; /** * 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 = "droidfu:extra_bitmap"; public static final String IMAGE_URL_EXTRA = "droidfu:extra_image_url"; private static final String LOG_TAG = "Droid-Fu/ImageLoader"; // the default thread pool size private static final int DEFAULT_POOL_SIZE = 320; // 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; /** * @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, DEFAULT_TTL_MINUTES, DEFAULT_POOL_SIZE); // imageCache.enableDiskCache(context, ImageCache.DISK_CACHE_SDCARD); // } } private String imageUrl; private ImageLoaderHandler handler; private 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 start(String imageUrl, ImageView imageView) { start(imageUrl, imageView, new ImageLoaderHandler(imageView, imageUrl)); } /** * 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 start(String imageUrl, ImageLoaderHandler handler) { start(imageUrl, handler.getImageView(), handler); } private static void start(String imageUrl, ImageView imageView, ImageLoaderHandler handler) { if (imageView != null) { String oldImageUrl = (String) imageView.getTag(); if (imageUrl.equals(oldImageUrl)) { // nothing to do return; } else { 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); Bitmap bitmap = null; if (bitmap == null) { bitmap = downloadImage(); } if (bitmap != null) { notifyImageLoaded(imageUrl, bitmap); } // TODO: notify about failure otherwise? } // 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(); // imageCache.put(imageUrl, imageData); return BitmapFactory.decodeByteArray(imageData, 0, imageData.length); } catch (Throwable e) { // Log.w(LOG_TAG, "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(); byte[] cachedImageData = null; cachedImageData = NetUtils.readImageData(imageUrl.trim(), Constant.ONE_YEAR); if (cachedImageData != null) { return cachedImageData; } // determine the image size and allocate a buffer int fileSize = connection.getContentLength(); byte[] imageData = new byte[fileSize]; // download the file // Log.d(LOG_TAG, "fetching image " + imageUrl + " (" + fileSize + ")"); 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(); if (imageData != null) { NetUtils.cacheImageInThread(imageUrl, imageData); } 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); data.putParcelable(BITMAP_EXTRA, bitmap); message.setData(data); handler.sendMessage(message); } }