• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 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.displayingbitmaps.util;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Bitmap;
22 import android.graphics.BitmapFactory;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.graphics.drawable.ColorDrawable;
25 import android.graphics.drawable.Drawable;
26 import android.graphics.drawable.TransitionDrawable;
27 import android.support.v4.app.FragmentActivity;
28 import android.support.v4.app.FragmentManager;
29 import android.widget.ImageView;
30 
31 import com.example.android.common.logger.Log;
32 import com.example.android.displayingbitmaps.BuildConfig;
33 
34 import java.lang.ref.WeakReference;
35 
36 /**
37  * This class wraps up completing some arbitrary long running work when loading a bitmap to an
38  * ImageView. It handles things like using a memory and disk cache, running the work in a background
39  * thread and setting a placeholder image.
40  */
41 public abstract class ImageWorker {
42     private static final String TAG = "ImageWorker";
43     private static final int FADE_IN_TIME = 200;
44 
45     private ImageCache mImageCache;
46     private ImageCache.ImageCacheParams mImageCacheParams;
47     private Bitmap mLoadingBitmap;
48     private boolean mFadeInBitmap = true;
49     private boolean mExitTasksEarly = false;
50     protected boolean mPauseWork = false;
51     private final Object mPauseWorkLock = new Object();
52 
53     protected Resources mResources;
54 
55     private static final int MESSAGE_CLEAR = 0;
56     private static final int MESSAGE_INIT_DISK_CACHE = 1;
57     private static final int MESSAGE_FLUSH = 2;
58     private static final int MESSAGE_CLOSE = 3;
59 
ImageWorker(Context context)60     protected ImageWorker(Context context) {
61         mResources = context.getResources();
62     }
63 
64     /**
65      * Load an image specified by the data parameter into an ImageView (override
66      * {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and
67      * disk cache will be used if an {@link ImageCache} has been added using
68      * {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the
69      * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask}
70      * will be created to asynchronously load the bitmap.
71      *
72      * @param data The URL of the image to download.
73      * @param imageView The ImageView to bind the downloaded image to.
74      */
loadImage(Object data, ImageView imageView)75     public void loadImage(Object data, ImageView imageView) {
76         if (data == null) {
77             return;
78         }
79 
80         BitmapDrawable value = null;
81 
82         if (mImageCache != null) {
83             value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
84         }
85 
86         if (value != null) {
87             // Bitmap found in memory cache
88             imageView.setImageDrawable(value);
89         } else if (cancelPotentialWork(data, imageView)) {
90             //BEGIN_INCLUDE(execute_background_task)
91             final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
92             final AsyncDrawable asyncDrawable =
93                     new AsyncDrawable(mResources, mLoadingBitmap, task);
94             imageView.setImageDrawable(asyncDrawable);
95 
96             // NOTE: This uses a custom version of AsyncTask that has been pulled from the
97             // framework and slightly modified. Refer to the docs at the top of the class
98             // for more info on what was changed.
99             task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR);
100             //END_INCLUDE(execute_background_task)
101         }
102     }
103 
104     /**
105      * Set placeholder bitmap that shows when the the background thread is running.
106      *
107      * @param bitmap
108      */
setLoadingImage(Bitmap bitmap)109     public void setLoadingImage(Bitmap bitmap) {
110         mLoadingBitmap = bitmap;
111     }
112 
113     /**
114      * Set placeholder bitmap that shows when the the background thread is running.
115      *
116      * @param resId
117      */
setLoadingImage(int resId)118     public void setLoadingImage(int resId) {
119         mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
120     }
121 
122     /**
123      * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap
124      * caching.
125      * @param fragmentManager
126      * @param cacheParams The cache parameters to use for the image cache.
127      */
addImageCache(FragmentManager fragmentManager, ImageCache.ImageCacheParams cacheParams)128     public void addImageCache(FragmentManager fragmentManager,
129             ImageCache.ImageCacheParams cacheParams) {
130         mImageCacheParams = cacheParams;
131         mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams);
132         new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
133     }
134 
135     /**
136      * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap
137      * caching.
138      * @param activity
139      * @param diskCacheDirectoryName See
140      * {@link ImageCache.ImageCacheParams#ImageCacheParams(android.content.Context, String)}.
141      */
addImageCache(FragmentActivity activity, String diskCacheDirectoryName)142     public void addImageCache(FragmentActivity activity, String diskCacheDirectoryName) {
143         mImageCacheParams = new ImageCache.ImageCacheParams(activity, diskCacheDirectoryName);
144         mImageCache = ImageCache.getInstance(activity.getSupportFragmentManager(), mImageCacheParams);
145         new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
146     }
147 
148     /**
149      * If set to true, the image will fade-in once it has been loaded by the background thread.
150      */
setImageFadeIn(boolean fadeIn)151     public void setImageFadeIn(boolean fadeIn) {
152         mFadeInBitmap = fadeIn;
153     }
154 
setExitTasksEarly(boolean exitTasksEarly)155     public void setExitTasksEarly(boolean exitTasksEarly) {
156         mExitTasksEarly = exitTasksEarly;
157         setPauseWork(false);
158     }
159 
160     /**
161      * Subclasses should override this to define any processing or work that must happen to produce
162      * the final bitmap. This will be executed in a background thread and be long running. For
163      * example, you could resize a large bitmap here, or pull down an image from the network.
164      *
165      * @param data The data to identify which image to process, as provided by
166      *            {@link ImageWorker#loadImage(Object, android.widget.ImageView)}
167      * @return The processed bitmap
168      */
processBitmap(Object data)169     protected abstract Bitmap processBitmap(Object data);
170 
171     /**
172      * @return The {@link ImageCache} object currently being used by this ImageWorker.
173      */
getImageCache()174     protected ImageCache getImageCache() {
175         return mImageCache;
176     }
177 
178     /**
179      * Cancels any pending work attached to the provided ImageView.
180      * @param imageView
181      */
cancelWork(ImageView imageView)182     public static void cancelWork(ImageView imageView) {
183         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
184         if (bitmapWorkerTask != null) {
185             bitmapWorkerTask.cancel(true);
186             if (BuildConfig.DEBUG) {
187                 final Object bitmapData = bitmapWorkerTask.mData;
188                 Log.d(TAG, "cancelWork - cancelled work for " + bitmapData);
189             }
190         }
191     }
192 
193     /**
194      * Returns true if the current work has been canceled or if there was no work in
195      * progress on this image view.
196      * Returns false if the work in progress deals with the same data. The work is not
197      * stopped in that case.
198      */
cancelPotentialWork(Object data, ImageView imageView)199     public static boolean cancelPotentialWork(Object data, ImageView imageView) {
200         //BEGIN_INCLUDE(cancel_potential_work)
201         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
202 
203         if (bitmapWorkerTask != null) {
204             final Object bitmapData = bitmapWorkerTask.mData;
205             if (bitmapData == null || !bitmapData.equals(data)) {
206                 bitmapWorkerTask.cancel(true);
207                 if (BuildConfig.DEBUG) {
208                     Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
209                 }
210             } else {
211                 // The same work is already in progress.
212                 return false;
213             }
214         }
215         return true;
216         //END_INCLUDE(cancel_potential_work)
217     }
218 
219     /**
220      * @param imageView Any imageView
221      * @return Retrieve the currently active work task (if any) associated with this imageView.
222      * null if there is no such task.
223      */
getBitmapWorkerTask(ImageView imageView)224     private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
225         if (imageView != null) {
226             final Drawable drawable = imageView.getDrawable();
227             if (drawable instanceof AsyncDrawable) {
228                 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
229                 return asyncDrawable.getBitmapWorkerTask();
230             }
231         }
232         return null;
233     }
234 
235     /**
236      * The actual AsyncTask that will asynchronously process the image.
237      */
238     private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> {
239         private Object mData;
240         private final WeakReference<ImageView> imageViewReference;
241 
BitmapWorkerTask(Object data, ImageView imageView)242         public BitmapWorkerTask(Object data, ImageView imageView) {
243             mData = data;
244             imageViewReference = new WeakReference<ImageView>(imageView);
245         }
246 
247         /**
248          * Background processing.
249          */
250         @Override
doInBackground(Void... params)251         protected BitmapDrawable doInBackground(Void... params) {
252             //BEGIN_INCLUDE(load_bitmap_in_background)
253             if (BuildConfig.DEBUG) {
254                 Log.d(TAG, "doInBackground - starting work");
255             }
256 
257             final String dataString = String.valueOf(mData);
258             Bitmap bitmap = null;
259             BitmapDrawable drawable = null;
260 
261             // Wait here if work is paused and the task is not cancelled
262             synchronized (mPauseWorkLock) {
263                 while (mPauseWork && !isCancelled()) {
264                     try {
265                         mPauseWorkLock.wait();
266                     } catch (InterruptedException e) {}
267                 }
268             }
269 
270             // If the image cache is available and this task has not been cancelled by another
271             // thread and the ImageView that was originally bound to this task is still bound back
272             // to this task and our "exit early" flag is not set then try and fetch the bitmap from
273             // the cache
274             if (mImageCache != null && !isCancelled() && getAttachedImageView() != null
275                     && !mExitTasksEarly) {
276                 bitmap = mImageCache.getBitmapFromDiskCache(dataString);
277             }
278 
279             // If the bitmap was not found in the cache and this task has not been cancelled by
280             // another thread and the ImageView that was originally bound to this task is still
281             // bound back to this task and our "exit early" flag is not set, then call the main
282             // process method (as implemented by a subclass)
283             if (bitmap == null && !isCancelled() && getAttachedImageView() != null
284                     && !mExitTasksEarly) {
285                 bitmap = processBitmap(mData);
286             }
287 
288             // If the bitmap was processed and the image cache is available, then add the processed
289             // bitmap to the cache for future use. Note we don't check if the task was cancelled
290             // here, if it was, and the thread is still running, we may as well add the processed
291             // bitmap to our cache as it might be used again in the future
292             if (bitmap != null) {
293                 if (Utils.hasHoneycomb()) {
294                     // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable
295                     drawable = new BitmapDrawable(mResources, bitmap);
296                 } else {
297                     // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable
298                     // which will recycle automagically
299                     drawable = new RecyclingBitmapDrawable(mResources, bitmap);
300                 }
301 
302                 if (mImageCache != null) {
303                     mImageCache.addBitmapToCache(dataString, drawable);
304                 }
305             }
306 
307             if (BuildConfig.DEBUG) {
308                 Log.d(TAG, "doInBackground - finished work");
309             }
310 
311             return drawable;
312             //END_INCLUDE(load_bitmap_in_background)
313         }
314 
315         /**
316          * Once the image is processed, associates it to the imageView
317          */
318         @Override
onPostExecute(BitmapDrawable value)319         protected void onPostExecute(BitmapDrawable value) {
320             //BEGIN_INCLUDE(complete_background_work)
321             // if cancel was called on this task or the "exit early" flag is set then we're done
322             if (isCancelled() || mExitTasksEarly) {
323                 value = null;
324             }
325 
326             final ImageView imageView = getAttachedImageView();
327             if (value != null && imageView != null) {
328                 if (BuildConfig.DEBUG) {
329                     Log.d(TAG, "onPostExecute - setting bitmap");
330                 }
331                 setImageDrawable(imageView, value);
332             }
333             //END_INCLUDE(complete_background_work)
334         }
335 
336         @Override
onCancelled(BitmapDrawable value)337         protected void onCancelled(BitmapDrawable value) {
338             super.onCancelled(value);
339             synchronized (mPauseWorkLock) {
340                 mPauseWorkLock.notifyAll();
341             }
342         }
343 
344         /**
345          * Returns the ImageView associated with this task as long as the ImageView's task still
346          * points to this task as well. Returns null otherwise.
347          */
getAttachedImageView()348         private ImageView getAttachedImageView() {
349             final ImageView imageView = imageViewReference.get();
350             final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
351 
352             if (this == bitmapWorkerTask) {
353                 return imageView;
354             }
355 
356             return null;
357         }
358     }
359 
360     /**
361      * A custom Drawable that will be attached to the imageView while the work is in progress.
362      * Contains a reference to the actual worker task, so that it can be stopped if a new binding is
363      * required, and makes sure that only the last started worker process can bind its result,
364      * independently of the finish order.
365      */
366     private static class AsyncDrawable extends BitmapDrawable {
367         private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
368 
AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask)369         public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
370             super(res, bitmap);
371             bitmapWorkerTaskReference =
372                 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
373         }
374 
getBitmapWorkerTask()375         public BitmapWorkerTask getBitmapWorkerTask() {
376             return bitmapWorkerTaskReference.get();
377         }
378     }
379 
380     /**
381      * Called when the processing is complete and the final drawable should be
382      * set on the ImageView.
383      *
384      * @param imageView
385      * @param drawable
386      */
setImageDrawable(ImageView imageView, Drawable drawable)387     private void setImageDrawable(ImageView imageView, Drawable drawable) {
388         if (mFadeInBitmap) {
389             // Transition drawable with a transparent drawable and the final drawable
390             final TransitionDrawable td =
391                     new TransitionDrawable(new Drawable[] {
392                             new ColorDrawable(android.R.color.transparent),
393                             drawable
394                     });
395             // Set background to loading bitmap
396             imageView.setBackgroundDrawable(
397                     new BitmapDrawable(mResources, mLoadingBitmap));
398 
399             imageView.setImageDrawable(td);
400             td.startTransition(FADE_IN_TIME);
401         } else {
402             imageView.setImageDrawable(drawable);
403         }
404     }
405 
406     /**
407      * Pause any ongoing background work. This can be used as a temporary
408      * measure to improve performance. For example background work could
409      * be paused when a ListView or GridView is being scrolled using a
410      * {@link android.widget.AbsListView.OnScrollListener} to keep
411      * scrolling smooth.
412      * <p>
413      * If work is paused, be sure setPauseWork(false) is called again
414      * before your fragment or activity is destroyed (for example during
415      * {@link android.app.Activity#onPause()}), or there is a risk the
416      * background thread will never finish.
417      */
setPauseWork(boolean pauseWork)418     public void setPauseWork(boolean pauseWork) {
419         synchronized (mPauseWorkLock) {
420             mPauseWork = pauseWork;
421             if (!mPauseWork) {
422                 mPauseWorkLock.notifyAll();
423             }
424         }
425     }
426 
427     protected class CacheAsyncTask extends AsyncTask<Object, Void, Void> {
428 
429         @Override
doInBackground(Object... params)430         protected Void doInBackground(Object... params) {
431             switch ((Integer)params[0]) {
432                 case MESSAGE_CLEAR:
433                     clearCacheInternal();
434                     break;
435                 case MESSAGE_INIT_DISK_CACHE:
436                     initDiskCacheInternal();
437                     break;
438                 case MESSAGE_FLUSH:
439                     flushCacheInternal();
440                     break;
441                 case MESSAGE_CLOSE:
442                     closeCacheInternal();
443                     break;
444             }
445             return null;
446         }
447     }
448 
initDiskCacheInternal()449     protected void initDiskCacheInternal() {
450         if (mImageCache != null) {
451             mImageCache.initDiskCache();
452         }
453     }
454 
clearCacheInternal()455     protected void clearCacheInternal() {
456         if (mImageCache != null) {
457             mImageCache.clearCache();
458         }
459     }
460 
flushCacheInternal()461     protected void flushCacheInternal() {
462         if (mImageCache != null) {
463             mImageCache.flush();
464         }
465     }
466 
closeCacheInternal()467     protected void closeCacheInternal() {
468         if (mImageCache != null) {
469             mImageCache.close();
470             mImageCache = null;
471         }
472     }
473 
clearCache()474     public void clearCache() {
475         new CacheAsyncTask().execute(MESSAGE_CLEAR);
476     }
477 
flushCache()478     public void flushCache() {
479         new CacheAsyncTask().execute(MESSAGE_FLUSH);
480     }
481 
closeCache()482     public void closeCache() {
483         new CacheAsyncTask().execute(MESSAGE_CLOSE);
484     }
485 }
486