Java tutorial
/* Copyright (c) 2009-2011 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 cn.edu.szjm.support.images.remote; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; import android.os.SystemClock; import android.util.Log; import android.widget.ImageView; import cn.edu.szjm.core.tasks.IgnitedAsyncTask; import cn.edu.szjm.core.tasks.IgnitedAsyncTask.IgnitedAsyncTaskCallable; import cn.edu.szjm.support.cache.ImageCache; import cn.edu.szjm.support.http.IgnitedHttp; import cn.edu.szjm.support.http.IgnitedHttpRequest; import cn.edu.szjm.util.SharedImageCache; /** * Realizes a background image loader that downloads an image from a URL, optionally 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 RemoteImageLoader { // the default thread pool size // expire images after a day // TODO: this currently only affects the in-memory cache, so it's quite pointless private static final int DEFAULT_NUM_RETRIES = 3; private static final int DEFAULT_BUFFER_SIZE = 65536; private static final String LOG_TAG = "Ignition/ImageLoader"; private static final int DEFAULT_RETRY_HANDLER_SLEEP_TIME = 1000; private String imageUrl; private ImageCache imageCache; private RemoteImageLoaderHandler handler; // private ThreadPoolExecutor executor; public IgnitedAsyncTask<Context, Void, Void, Bitmap> task; private boolean usingCache; private int numRetries = DEFAULT_NUM_RETRIES; private int defaultBufferSize = DEFAULT_BUFFER_SIZE; private boolean cancel = false; private Drawable defaultDummyDrawable, errorDrawable; public RemoteImageLoader(Context context) { this(context, true); } /** * Creates a new ImageLoader that is backed by an {@link ImageCache}. The cache will by default * cache to the device's external storage, and expire images after 1 day. You can set useCache * to false and then supply your own image cache instance via {@link #setImageCache(ImageCache)} * , or fine-tune the default one through {@link #getImageCache()}. * * @param context * the current context * @param createCache * whether to create a default {@link ImageCache} used for caching */ public RemoteImageLoader(final Context context, boolean usingCache) { // executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(DEFAULT_POOL_SIZE); this.usingCache = usingCache; if (usingCache) { imageCache = SharedImageCache.getSharedImageCache(); } else { imageCache = null; } errorDrawable = context.getResources().getDrawable(android.R.drawable.ic_dialog_alert); task = new IgnitedAsyncTask<Context, Void, Void, Bitmap>() { @Override public boolean onTaskCompleted(Bitmap bitmap) { if (!cancel) { handler.handleImageLoaded(bitmap, null); } return true; } @Override public boolean onTaskFailed(Exception error) { if (task != null) { Log.d("test", "failed " + task.getStatus() + task); } return true; } // @Override // protected void onCancelled() { // if(task != null) { // Log.d("test", "cancel "+task.getStatus() + task + isCancelled()); // } // } }; IgnitedAsyncTaskCallable<Context, Void, Void, Bitmap> callable = new IgnitedAsyncTaskCallable<Context, Void, Void, Bitmap>() { @Override public Bitmap call(IgnitedAsyncTask<Context, Void, Void, Bitmap> task) throws Exception { // TODO Auto-generated method stub if (cancel) { return null; } Bitmap bitmap = null; if (imageCache != null) { // at this point we know the image is not in memory, but it could be cached to SD card bitmap = imageCache.getBitmap(imageUrl); } if (bitmap == null) { bitmap = downloadImage(context); } return bitmap; } protected Bitmap downloadImage(Context context) { int timesTried = 1; while (timesTried <= numRetries && !cancel) { try { if (cancel) { return null; } byte[] imageData = retrieveImageData(context); if (imageData == null) { break; } if (imageCache != null) { 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(Context context) throws IOException { IgnitedHttp http = new IgnitedHttp(context); http.disableResponseCache(true); IgnitedHttpRequest request = http.get(imageUrl, true).retries(3).expecting(200); request.setWrapResponse(false); if (cancel) { return null; } HttpResponse response = (HttpResponse) request.send(); HttpEntity entity = response.getEntity(); // URL url = new URL(imageUrl); // HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // determine the image size and allocate a buffer int fileSize = (int) entity.getContentLength(); Log.d(LOG_TAG, "fetching image " + imageUrl + " (" + (fileSize <= 0 ? "size unknown" : Integer.toString(fileSize)) + ")"); BufferedInputStream istream = new BufferedInputStream(entity.getContent()); try { if (fileSize <= 0) { Log.w(LOG_TAG, "Server did not set a Content-Length header, will default to buffer size of " + defaultBufferSize + " bytes"); ByteArrayOutputStream buf = new ByteArrayOutputStream(defaultBufferSize); byte[] buffer = new byte[defaultBufferSize]; int bytesRead = 0; while (bytesRead != -1) { if (cancel) { return null; } bytesRead = istream.read(buffer, 0, defaultBufferSize); if (bytesRead > 0) buf.write(buffer, 0, bytesRead); } return buf.toByteArray(); } else { byte[] imageData = new byte[fileSize]; int bytesRead = 0; int offset = 0; while (bytesRead != -1 && offset < fileSize) { if (cancel) { return null; } bytesRead = istream.read(imageData, offset, fileSize - offset); offset += bytesRead; } return imageData; } } finally { // clean up try { istream.close(); //connection.disconnect(); } catch (Exception ignore) { } } } }; task.setCallable(callable); } public void cancel() { cancel = true; task.cancel(true); task = null; } // /** // * @param numThreads // * the maximum number of threads that will be started to download images in parallel // */ // public void setThreadPoolSize(int numThreads) { // executor.setMaximumPoolSize(numThreads); // } /** * @param numAttempts * how often the image loader should retry the image download if network connection * fails */ public void setMaxDownloadAttempts(int numAttempts) { numRetries = numAttempts; } /** * If the server you're loading images from does not report file sizes via the Content-Length * header, then you can use this method to tell the downloader how much space it should allocate * by default when downloading an image into memory. * * @param defaultBufferSize * how big the buffer should be into which the image file is read. This should be big * enough to hold the largest image you expect to download */ public void setDefaultBufferSize(int defaultBufferSize) { this.defaultBufferSize = defaultBufferSize; } public void setDownloadInProgressDrawable(Drawable drawable) { this.defaultDummyDrawable = drawable; } public void setDownloadFailedDrawable(Drawable drawable) { this.errorDrawable = drawable; } /** * 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. This method will the default * {@link RemoteImageLoaderHandler} to process the bitmap after downloading it. * * @param imageUrl * the URL of the image to download * @param imageView * the ImageView which should be updated with the new image */ public void loadImage(String imageUrl, ImageView imageView) { loadImage(imageUrl, imageView, defaultDummyDrawable, new RemoteImageLoaderHandler(imageView, imageUrl, errorDrawable)); } /** * 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. This method will the default * {@link RemoteImageLoaderHandler} to process the bitmap after downloading it. * * @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 to be shown while the image is being downloaded. */ public void loadImage(String imageUrl, ImageView imageView, Drawable dummyDrawable) { loadImage(imageUrl, imageView, dummyDrawable, new RemoteImageLoaderHandler(imageView, imageUrl, errorDrawable)); } /** * 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 * @param handler * the handler that will process the bitmap after completion */ public void loadImage(String imageUrl, ImageView imageView, RemoteImageLoaderHandler handler) { loadImage(imageUrl, imageView, defaultDummyDrawable, 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. While waiting, the dummyDrawable is * shown. * * @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 to be shown while the image is being downloaded. * @param handler * the handler that will process the bitmap after completion */ public void loadImage(String imageUrl, ImageView imageView, Drawable dummyDrawable, RemoteImageLoaderHandler handler) { this.imageUrl = imageUrl; this.handler = handler; 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); if (dummyDrawable != null) { imageView.setImageDrawable(dummyDrawable); } return; } String oldImageUrl = (String) imageView.getTag(); if (imageUrl.equals(oldImageUrl)) { // nothing to do return; } else { if (dummyDrawable != null) { // Set the dummy image while waiting for the actual image to be downloaded. imageView.setImageDrawable(dummyDrawable); } imageView.setTag(imageUrl); } } ImageCache cache = SharedImageCache.getSharedImageCache(); if (usingCache) { if (cache != null && cache.containsKeyInMemory(imageUrl)) { // do not go through message passing, handle directly instead handler.handleImageLoaded(cache.getBitmap(imageUrl), null); return; } } task.execute(); } }