DisplayingBitmaps / src / com.example.android.displayingbitmaps / util /

ImageWorker.java

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)) {
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);
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) {
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;
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) {
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;
313
        }
314
 
315
        /**
316
         * Once the image is processed, associates it to the imageView
317
         */
318
        @Override
319
        protected void onPostExecute(BitmapDrawable value) {
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
            }
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
}