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