Home | History | Annotate | Download | only in xmladapters
      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      */
     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      */
    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      */
    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      */
    152     public void clearCache() {
    153         sHardBitmapCache.clear();
    154         sSoftBitmapCache.clear();
    155     }
    156 
    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      */
    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      */
    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      */
    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 
    240         public BitmapDownloaderTask(ImageView imageView) {
    241             imageViewReference = new WeakReference<ImageView>(imageView);
    242         }
    243 
    244         /**
    245          * Actual download method.
    246          */
    247         @Override
    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
    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 
    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 
    357         public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
    358             super(Color.BLACK);
    359             bitmapDownloaderTaskReference =
    360                 new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
    361         }
    362 
    363         public BitmapDownloaderTask getBitmapDownloaderTask() {
    364             return bitmapDownloaderTaskReference.get();
    365         }
    366     }
    367 }
    368