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      */
     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      */
    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      */
    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      */
    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      */
    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      */
    151     public void setImageFadeIn(boolean fadeIn) {
    152         mFadeInBitmap = fadeIn;
    153     }
    154 
    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      */
    169     protected abstract Bitmap processBitmap(Object data);
    170 
    171     /**
    172      * @return The {@link ImageCache} object currently being used by this ImageWorker.
    173      */
    174     protected ImageCache getImageCache() {
    175         return mImageCache;
    176     }
    177 
    178     /**
    179      * Cancels any pending work attached to the provided ImageView.
    180      * @param imageView
    181      */
    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      */
    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      */
    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 
    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
    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
    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
    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          */
    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 
    369         public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
    370             super(res, bitmap);
    371             bitmapWorkerTaskReference =
    372                 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    373         }
    374 
    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      */
    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      */
    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
    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 
    449     protected void initDiskCacheInternal() {
    450         if (mImageCache != null) {
    451             mImageCache.initDiskCache();
    452         }
    453     }
    454 
    455     protected void clearCacheInternal() {
    456         if (mImageCache != null) {
    457             mImageCache.clearCache();
    458         }
    459     }
    460 
    461     protected void flushCacheInternal() {
    462         if (mImageCache != null) {
    463             mImageCache.flush();
    464         }
    465     }
    466 
    467     protected void closeCacheInternal() {
    468         if (mImageCache != null) {
    469             mImageCache.close();
    470             mImageCache = null;
    471         }
    472     }
    473 
    474     public void clearCache() {
    475         new CacheAsyncTask().execute(MESSAGE_CLEAR);
    476     }
    477 
    478     public void flushCache() {
    479         new CacheAsyncTask().execute(MESSAGE_FLUSH);
    480     }
    481 
    482     public void closeCache() {
    483         new CacheAsyncTask().execute(MESSAGE_CLOSE);
    484     }
    485 }
    486