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