Home | History | Annotate | Download | only in util
      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 
     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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    170     public void setImageFadeIn(boolean fadeIn) {
    171         mFadeInBitmap = fadeIn;
    172     }
    173 
    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      */
    188     protected abstract Bitmap processBitmap(Object data);
    189 
    190     /**
    191      * @return The {@link ImageCache} object currently being used by this ImageWorker.
    192      */
    193     protected ImageCache getImageCache() {
    194         return mImageCache;
    195     }
    196 
    197     /**
    198      * Cancels any pending work attached to the provided ImageView.
    199      * @param imageView
    200      */
    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      */
    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      */
    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 
    262         public BitmapWorkerTask(Object data, ImageView imageView) {
    263             mData = data;
    264             imageViewReference = new WeakReference<ImageView>(imageView);
    265             mOnImageLoadedListener = null;
    266         }
    267 
    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
    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
    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
    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          */
    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          */
    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 
    414         public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
    415             super(res, bitmap);
    416             bitmapWorkerTaskReference =
    417                 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    418         }
    419 
    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      */
    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      */
    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
    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 
    494     protected void initDiskCacheInternal() {
    495         if (mImageCache != null) {
    496             mImageCache.initDiskCache();
    497         }
    498     }
    499 
    500     protected void clearCacheInternal() {
    501         if (mImageCache != null) {
    502             mImageCache.clearCache();
    503         }
    504     }
    505 
    506     protected void flushCacheInternal() {
    507         if (mImageCache != null) {
    508             mImageCache.flush();
    509         }
    510     }
    511 
    512     protected void closeCacheInternal() {
    513         if (mImageCache != null) {
    514             mImageCache.close();
    515             mImageCache = null;
    516         }
    517     }
    518 
    519     public void clearCache() {
    520         new CacheAsyncTask().execute(MESSAGE_CLEAR);
    521     }
    522 
    523     public void flushCache() {
    524         new CacheAsyncTask().execute(MESSAGE_FLUSH);
    525     }
    526 
    527     public void closeCache() {
    528         new CacheAsyncTask().execute(MESSAGE_CLOSE);
    529     }
    530 }
    531