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