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