1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.example.android.xmladapters; 18 19 import org.apache.http.HttpEntity; 20 import org.apache.http.HttpResponse; 21 import org.apache.http.HttpStatus; 22 import org.apache.http.client.methods.HttpGet; 23 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.Color; 27 import android.graphics.drawable.ColorDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.net.http.AndroidHttpClient; 30 import android.os.AsyncTask; 31 import android.os.Handler; 32 import android.util.Log; 33 import android.widget.ImageView; 34 35 import java.io.BufferedOutputStream; 36 import java.io.ByteArrayOutputStream; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.io.OutputStream; 40 import java.lang.ref.SoftReference; 41 import java.lang.ref.WeakReference; 42 import java.util.HashMap; 43 import java.util.LinkedHashMap; 44 import java.util.Map; 45 import java.util.concurrent.ConcurrentHashMap; 46 47 /** 48 * This helper class download images from the Internet and binds those with the provided ImageView. 49 * 50 * <p>It requires the INTERNET permission, which should be added to your application's manifest 51 * file.</p> 52 * 53 * A local cache of downloaded images is maintained internally to improve performance. 54 */ 55 public class ImageDownloader { 56 private static final String LOG_TAG = "ImageDownloader"; 57 58 private static final int HARD_CACHE_CAPACITY = 40; 59 private static final int DELAY_BEFORE_PURGE = 30 * 1000; // in milliseconds 60 61 // Hard cache, with a fixed maximum capacity and a life duration 62 private final static HashMap<String, Bitmap> sHardBitmapCache = 63 new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true) { 64 private static final long serialVersionUID = -7190622541619388252L; 65 @Override 66 protected boolean removeEldestEntry(Map.Entry<String, Bitmap> eldest) { 67 if (size() > HARD_CACHE_CAPACITY) { 68 // Entries push-out of hard reference cache are transferred to soft reference cache 69 sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue())); 70 return true; 71 } else { 72 return false; 73 } 74 } 75 }; 76 77 // Soft cache for bitmap kicked out of hard cache 78 private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache = 79 new ConcurrentHashMap<String, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2); 80 81 private final Handler purgeHandler = new Handler(); 82 83 private final Runnable purger = new Runnable() { 84 public void run() { 85 clearCache(); 86 } 87 }; 88 89 /** 90 * Download the specified image from the Internet and binds it to the provided ImageView. The 91 * binding is immediate if the image is found in the cache and will be done asynchronously 92 * otherwise. A null bitmap will be associated to the ImageView if an error occurs. 93 * 94 * @param url The URL of the image to download. 95 * @param imageView The ImageView to bind the downloaded image to. 96 */ download(String url, ImageView imageView)97 public void download(String url, ImageView imageView) { 98 download(url, imageView, null); 99 } 100 101 /** 102 * Same as {@link #download(String, ImageView)}, with the possibility to provide an additional 103 * cookie that will be used when the image will be retrieved. 104 * 105 * @param url The URL of the image to download. 106 * @param imageView The ImageView to bind the downloaded image to. 107 * @param cookie A cookie String that will be used by the http connection. 108 */ download(String url, ImageView imageView, String cookie)109 public void download(String url, ImageView imageView, String cookie) { 110 resetPurgeTimer(); 111 Bitmap bitmap = getBitmapFromCache(url); 112 113 if (bitmap == null) { 114 forceDownload(url, imageView, cookie); 115 } else { 116 cancelPotentialDownload(url, imageView); 117 imageView.setImageBitmap(bitmap); 118 } 119 } 120 121 /* 122 * Same as download but the image is always downloaded and the cache is not used. 123 * Kept private at the moment as its interest is not clear. 124 private void forceDownload(String url, ImageView view) { 125 forceDownload(url, view, null); 126 } 127 */ 128 129 /** 130 * Same as download but the image is always downloaded and the cache is not used. 131 * Kept private at the moment as its interest is not clear. 132 */ forceDownload(String url, ImageView imageView, String cookie)133 private void forceDownload(String url, ImageView imageView, String cookie) { 134 // State sanity: url is guaranteed to never be null in DownloadedDrawable and cache keys. 135 if (url == null) { 136 imageView.setImageDrawable(null); 137 return; 138 } 139 140 if (cancelPotentialDownload(url, imageView)) { 141 BitmapDownloaderTask task = new BitmapDownloaderTask(imageView); 142 DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task); 143 imageView.setImageDrawable(downloadedDrawable); 144 task.execute(url, cookie); 145 } 146 } 147 148 /** 149 * Clears the image cache used internally to improve performance. Note that for memory 150 * efficiency reasons, the cache will automatically be cleared after a certain inactivity delay. 151 */ clearCache()152 public void clearCache() { 153 sHardBitmapCache.clear(); 154 sSoftBitmapCache.clear(); 155 } 156 resetPurgeTimer()157 private void resetPurgeTimer() { 158 purgeHandler.removeCallbacks(purger); 159 purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE); 160 } 161 162 /** 163 * Returns true if the current download has been canceled or if there was no download in 164 * progress on this image view. 165 * Returns false if the download in progress deals with the same url. The download is not 166 * stopped in that case. 167 */ cancelPotentialDownload(String url, ImageView imageView)168 private static boolean cancelPotentialDownload(String url, ImageView imageView) { 169 BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView); 170 171 if (bitmapDownloaderTask != null) { 172 String bitmapUrl = bitmapDownloaderTask.url; 173 if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) { 174 bitmapDownloaderTask.cancel(true); 175 } else { 176 // The same URL is already being downloaded. 177 return false; 178 } 179 } 180 return true; 181 } 182 183 /** 184 * @param imageView Any imageView 185 * @return Retrieve the currently active download task (if any) associated with this imageView. 186 * null if there is no such task. 187 */ getBitmapDownloaderTask(ImageView imageView)188 private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) { 189 if (imageView != null) { 190 Drawable drawable = imageView.getDrawable(); 191 if (drawable instanceof DownloadedDrawable) { 192 DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable; 193 return downloadedDrawable.getBitmapDownloaderTask(); 194 } 195 } 196 return null; 197 } 198 199 /** 200 * @param url The URL of the image that will be retrieved from the cache. 201 * @return The cached bitmap or null if it was not found. 202 */ getBitmapFromCache(String url)203 private Bitmap getBitmapFromCache(String url) { 204 // First try the hard reference cache 205 synchronized (sHardBitmapCache) { 206 final Bitmap bitmap = sHardBitmapCache.get(url); 207 if (bitmap != null) { 208 // Bitmap found in hard cache 209 // Move element to first position, so that it is removed last 210 sHardBitmapCache.remove(url); 211 sHardBitmapCache.put(url, bitmap); 212 return bitmap; 213 } 214 } 215 216 // Then try the soft reference cache 217 SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(url); 218 if (bitmapReference != null) { 219 final Bitmap bitmap = bitmapReference.get(); 220 if (bitmap != null) { 221 // Bitmap found in soft cache 222 return bitmap; 223 } else { 224 // Soft reference has been Garbage Collected 225 sSoftBitmapCache.remove(url); 226 } 227 } 228 229 return null; 230 } 231 232 /** 233 * The actual AsyncTask that will asynchronously download the image. 234 */ 235 class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> { 236 private static final int IO_BUFFER_SIZE = 4 * 1024; 237 private String url; 238 private final WeakReference<ImageView> imageViewReference; 239 BitmapDownloaderTask(ImageView imageView)240 public BitmapDownloaderTask(ImageView imageView) { 241 imageViewReference = new WeakReference<ImageView>(imageView); 242 } 243 244 /** 245 * Actual download method. 246 */ 247 @Override doInBackground(String... params)248 protected Bitmap doInBackground(String... params) { 249 final AndroidHttpClient client = AndroidHttpClient.newInstance("Android"); 250 url = params[0]; 251 final HttpGet getRequest = new HttpGet(url); 252 String cookie = params[1]; 253 if (cookie != null) { 254 getRequest.setHeader("cookie", cookie); 255 } 256 257 try { 258 HttpResponse response = client.execute(getRequest); 259 final int statusCode = response.getStatusLine().getStatusCode(); 260 if (statusCode != HttpStatus.SC_OK) { 261 Log.w("ImageDownloader", "Error " + statusCode + 262 " while retrieving bitmap from " + url); 263 return null; 264 } 265 266 final HttpEntity entity = response.getEntity(); 267 if (entity != null) { 268 InputStream inputStream = null; 269 OutputStream outputStream = null; 270 try { 271 inputStream = entity.getContent(); 272 final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); 273 outputStream = new BufferedOutputStream(dataStream, IO_BUFFER_SIZE); 274 copy(inputStream, outputStream); 275 outputStream.flush(); 276 277 final byte[] data = dataStream.toByteArray(); 278 final Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); 279 280 // FIXME : Should use BitmapFactory.decodeStream(inputStream) instead. 281 //final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); 282 283 return bitmap; 284 285 } finally { 286 if (inputStream != null) { 287 inputStream.close(); 288 } 289 if (outputStream != null) { 290 outputStream.close(); 291 } 292 entity.consumeContent(); 293 } 294 } 295 } catch (IOException e) { 296 getRequest.abort(); 297 Log.w(LOG_TAG, "I/O error while retrieving bitmap from " + url, e); 298 } catch (IllegalStateException e) { 299 getRequest.abort(); 300 Log.w(LOG_TAG, "Incorrect URL: " + url); 301 } catch (Exception e) { 302 getRequest.abort(); 303 Log.w(LOG_TAG, "Error while retrieving bitmap from " + url, e); 304 } finally { 305 if (client != null) { 306 client.close(); 307 } 308 } 309 return null; 310 } 311 312 /** 313 * Once the image is downloaded, associates it to the imageView 314 */ 315 @Override onPostExecute(Bitmap bitmap)316 protected void onPostExecute(Bitmap bitmap) { 317 if (isCancelled()) { 318 bitmap = null; 319 } 320 321 // Add bitmap to cache 322 if (bitmap != null) { 323 synchronized (sHardBitmapCache) { 324 sHardBitmapCache.put(url, bitmap); 325 } 326 } 327 328 if (imageViewReference != null) { 329 ImageView imageView = imageViewReference.get(); 330 BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView); 331 // Change bitmap only if this process is still associated with it 332 if (this == bitmapDownloaderTask) { 333 imageView.setImageBitmap(bitmap); 334 } 335 } 336 } 337 copy(InputStream in, OutputStream out)338 public void copy(InputStream in, OutputStream out) throws IOException { 339 byte[] b = new byte[IO_BUFFER_SIZE]; 340 int read; 341 while ((read = in.read(b)) != -1) { 342 out.write(b, 0, read); 343 } 344 } 345 } 346 347 /** 348 * A fake Drawable that will be attached to the imageView while the download is in progress. 349 * 350 * <p>Contains a reference to the actual download task, so that a download task can be stopped 351 * if a new binding is required, and makes sure that only the last started download process can 352 * bind its result, independently of the download finish order.</p> 353 */ 354 static class DownloadedDrawable extends ColorDrawable { 355 private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference; 356 DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask)357 public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) { 358 super(Color.BLACK); 359 bitmapDownloaderTaskReference = 360 new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask); 361 } 362 getBitmapDownloaderTask()363 public BitmapDownloaderTask getBitmapDownloaderTask() { 364 return bitmapDownloaderTaskReference.get(); 365 } 366 } 367 } 368