• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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