1 /*
2  * Copyright (C) 2013 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.contactslist.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.os.AsyncTask;
28 import android.support.v4.app.FragmentManager;
29 import android.util.Log;
30 import android.widget.ImageView;
31 
32 import com.example.android.contactslist.BuildConfig;
33 
34 import java.io.FileDescriptor;
35 import java.lang.ref.WeakReference;
36 
37 /**
38  * This class wraps up completing some arbitrary long running work when loading a bitmap to an
39  * ImageView. It handles things like using a memory and disk cache, running the work in a background
40  * thread and setting a placeholder image.
41  */
42 public abstract class ImageLoader {
43     private static final String TAG = "ImageLoader";
44     private static final int FADE_IN_TIME = 200;
45 
46     private ImageCache mImageCache;
47     private Bitmap mLoadingBitmap;
48     private boolean mFadeInBitmap = true;
49     private boolean mPauseWork = false;
50     private final Object mPauseWorkLock = new Object();
51     private int mImageSize;
52     private Resources mResources;
53 
ImageLoader(Context context, int imageSize)54     protected ImageLoader(Context context, int imageSize) {
55         mResources = context.getResources();
56         mImageSize = imageSize;
57     }
58 
getImageSize()59     public int getImageSize() {
60         return mImageSize;
61     }
62 
63     /**
64      * Load an image specified by the data parameter into an ImageView (override
65      * {@link ImageLoader#processBitmap(Object)} to define the processing logic). If the image is
66      * found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} will be
67      * created to asynchronously load the bitmap.
68      *
69      * @param data The URL of the image to download.
70      * @param imageView The ImageView to bind the downloaded image to.
71      */
loadImage(Object data, ImageView imageView)72     public void loadImage(Object data, ImageView imageView) {
73         if (data == null) {
74             imageView.setImageBitmap(mLoadingBitmap);
75             return;
76         }
77 
78         Bitmap bitmap = null;
79 
80         if (mImageCache != null) {
81             bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data));
82         }
83 
84         if (bitmap != null) {
85             // Bitmap found in memory cache
86             imageView.setImageBitmap(bitmap);
87         } else if (cancelPotentialWork(data, imageView)) {
88             final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
89             final AsyncDrawable asyncDrawable =
90                     new AsyncDrawable(mResources, mLoadingBitmap, task);
91             imageView.setImageDrawable(asyncDrawable);
92             task.execute(data);
93         }
94     }
95 
96     /**
97      * Set placeholder bitmap that shows when the the background thread is running.
98      *
99      * @param resId Resource ID of loading image.
100      */
setLoadingImage(int resId)101     public void setLoadingImage(int resId) {
102         mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
103     }
104 
105     /**
106      * Adds an {@link ImageCache} to this image loader.
107      *
108      * @param fragmentManager A FragmentManager to use to retain the cache over configuration
109      *                        changes such as an orientation change.
110      * @param memCacheSizePercent The cache size as a percent of available app memory.
111      */
addImageCache(FragmentManager fragmentManager, float memCacheSizePercent)112     public void addImageCache(FragmentManager fragmentManager, float memCacheSizePercent) {
113         mImageCache = ImageCache.getInstance(fragmentManager, memCacheSizePercent);
114     }
115 
116     /**
117      * If set to true, the image will fade-in once it has been loaded by the background thread.
118      */
setImageFadeIn(boolean fadeIn)119     public void setImageFadeIn(boolean fadeIn) {
120         mFadeInBitmap = fadeIn;
121     }
122 
123     /**
124      * Subclasses should override this to define any processing or work that must happen to produce
125      * the final bitmap. This will be executed in a background thread and be long running. For
126      * example, you could resize a large bitmap here, or pull down an image from the network.
127      *
128      * @param data The data to identify which image to process, as provided by
129      *            {@link ImageLoader#loadImage(Object, ImageView)}
130      * @return The processed bitmap
131      */
processBitmap(Object data)132     protected abstract Bitmap processBitmap(Object data);
133 
134     /**
135      * Cancels any pending work attached to the provided ImageView.
136      */
cancelWork(ImageView imageView)137     public static void cancelWork(ImageView imageView) {
138         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
139         if (bitmapWorkerTask != null) {
140             bitmapWorkerTask.cancel(true);
141             if (BuildConfig.DEBUG) {
142                 final Object bitmapData = bitmapWorkerTask.data;
143                 Log.d(TAG, "cancelWork - cancelled work for " + bitmapData);
144             }
145         }
146     }
147 
148     /**
149      * Returns true if the current work has been canceled or if there was no work in
150      * progress on this image view.
151      * Returns false if the work in progress deals with the same data. The work is not
152      * stopped in that case.
153      */
cancelPotentialWork(Object data, ImageView imageView)154     public static boolean cancelPotentialWork(Object data, ImageView imageView) {
155         final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
156 
157         if (bitmapWorkerTask != null) {
158             final Object bitmapData = bitmapWorkerTask.data;
159             if (bitmapData == null || !bitmapData.equals(data)) {
160                 bitmapWorkerTask.cancel(true);
161                 if (BuildConfig.DEBUG) {
162                     Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
163                 }
164             } else {
165                 // The same work is already in progress.
166                 return false;
167             }
168         }
169         return true;
170     }
171 
172     /**
173      * @param imageView Any imageView
174      * @return Retrieve the currently active work task (if any) associated with this imageView.
175      * null if there is no such task.
176      */
getBitmapWorkerTask(ImageView imageView)177     private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
178         if (imageView != null) {
179             final Drawable drawable = imageView.getDrawable();
180             if (drawable instanceof AsyncDrawable) {
181                 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
182                 return asyncDrawable.getBitmapWorkerTask();
183             }
184         }
185         return null;
186     }
187 
188     /**
189      * The actual AsyncTask that will asynchronously process the image.
190      */
191     private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> {
192         private Object data;
193         private final WeakReference<ImageView> imageViewReference;
194 
BitmapWorkerTask(ImageView imageView)195         public BitmapWorkerTask(ImageView imageView) {
196             imageViewReference = new WeakReference<ImageView>(imageView);
197         }
198 
199         /**
200          * Background processing.
201          */
202         @Override
doInBackground(Object... params)203         protected Bitmap doInBackground(Object... params) {
204             if (BuildConfig.DEBUG) {
205                 Log.d(TAG, "doInBackground - starting work");
206             }
207 
208             data = params[0];
209             final String dataString = String.valueOf(data);
210             Bitmap bitmap = null;
211 
212             // Wait here if work is paused and the task is not cancelled
213             synchronized (mPauseWorkLock) {
214                 while (mPauseWork && !isCancelled()) {
215                     try {
216                         mPauseWorkLock.wait();
217                     } catch (InterruptedException e) {}
218                 }
219             }
220 
221             // If the task has not been cancelled by another thread and the ImageView that was
222             // originally bound to this task is still bound back to this task and our "exit early"
223             // flag is not set, then call the main process method (as implemented by a subclass)
224             if (!isCancelled() && getAttachedImageView() != null) {
225                 bitmap = processBitmap(params[0]);
226             }
227 
228             // If the bitmap was processed and the image cache is available, then add the processed
229             // bitmap to the cache for future use. Note we don't check if the task was cancelled
230             // here, if it was, and the thread is still running, we may as well add the processed
231             // bitmap to our cache as it might be used again in the future
232             if (bitmap != null && mImageCache != null) {
233                 mImageCache.addBitmapToCache(dataString, bitmap);
234             }
235 
236             if (BuildConfig.DEBUG) {
237                 Log.d(TAG, "doInBackground - finished work");
238             }
239 
240             return bitmap;
241         }
242 
243         /**
244          * Once the image is processed, associates it to the imageView
245          */
246         @Override
onPostExecute(Bitmap bitmap)247         protected void onPostExecute(Bitmap bitmap) {
248             // if cancel was called on this task or the "exit early" flag is set then we're done
249             if (isCancelled()) {
250                 bitmap = null;
251             }
252 
253             final ImageView imageView = getAttachedImageView();
254             if (bitmap != null && imageView != null) {
255                 if (BuildConfig.DEBUG) {
256                     Log.d(TAG, "onPostExecute - setting bitmap");
257                 }
258                 setImageBitmap(imageView, bitmap);
259             }
260         }
261 
262         @Override
onCancelled(Bitmap bitmap)263         protected void onCancelled(Bitmap bitmap) {
264             super.onCancelled(bitmap);
265             synchronized (mPauseWorkLock) {
266                 mPauseWorkLock.notifyAll();
267             }
268         }
269 
270         /**
271          * Returns the ImageView associated with this task as long as the ImageView's task still
272          * points to this task as well. Returns null otherwise.
273          */
getAttachedImageView()274         private ImageView getAttachedImageView() {
275             final ImageView imageView = imageViewReference.get();
276             final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
277 
278             if (this == bitmapWorkerTask) {
279                 return imageView;
280             }
281 
282             return null;
283         }
284     }
285 
286     /**
287      * A custom Drawable that will be attached to the imageView while the work is in progress.
288      * Contains a reference to the actual worker task, so that it can be stopped if a new binding is
289      * required, and makes sure that only the last started worker process can bind its result,
290      * independently of the finish order.
291      */
292     private static class AsyncDrawable extends BitmapDrawable {
293         private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
294 
AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask)295         public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
296             super(res, bitmap);
297             bitmapWorkerTaskReference =
298                 new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
299         }
300 
getBitmapWorkerTask()301         public BitmapWorkerTask getBitmapWorkerTask() {
302             return bitmapWorkerTaskReference.get();
303         }
304     }
305 
306     /**
307      * Called when the processing is complete and the final bitmap should be set on the ImageView.
308      *
309      * @param imageView The ImageView to set the bitmap to.
310      * @param bitmap The new bitmap to set.
311      */
setImageBitmap(ImageView imageView, Bitmap bitmap)312     private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
313         if (mFadeInBitmap) {
314             // Transition drawable to fade from loading bitmap to final bitmap
315             final TransitionDrawable td =
316                     new TransitionDrawable(new Drawable[] {
317                             new ColorDrawable(android.R.color.transparent),
318                             new BitmapDrawable(mResources, bitmap)
319                     });
320             imageView.setBackgroundDrawable(imageView.getDrawable());
321             imageView.setImageDrawable(td);
322             td.startTransition(FADE_IN_TIME);
323         } else {
324             imageView.setImageBitmap(bitmap);
325         }
326     }
327 
328     /**
329      * Pause any ongoing background work. This can be used as a temporary
330      * measure to improve performance. For example background work could
331      * be paused when a ListView or GridView is being scrolled using a
332      * {@link android.widget.AbsListView.OnScrollListener} to keep
333      * scrolling smooth.
334      * <p>
335      * If work is paused, be sure setPauseWork(false) is called again
336      * before your fragment or activity is destroyed (for example during
337      * {@link android.app.Activity#onPause()}), or there is a risk the
338      * background thread will never finish.
339      */
setPauseWork(boolean pauseWork)340     public void setPauseWork(boolean pauseWork) {
341         synchronized (mPauseWorkLock) {
342             mPauseWork = pauseWork;
343             if (!mPauseWork) {
344                 mPauseWorkLock.notifyAll();
345             }
346         }
347     }
348 
349     /**
350      * Decode and sample down a bitmap from a file input stream to the requested width and height.
351      *
352      * @param fileDescriptor The file descriptor to read from
353      * @param reqWidth The requested width of the resulting bitmap
354      * @param reqHeight The requested height of the resulting bitmap
355      * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
356      *         that are equal to or greater than the requested width and height
357      */
decodeSampledBitmapFromDescriptor( FileDescriptor fileDescriptor, int reqWidth, int reqHeight)358     public static Bitmap decodeSampledBitmapFromDescriptor(
359             FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {
360 
361         // First decode with inJustDecodeBounds=true to check dimensions
362         final BitmapFactory.Options options = new BitmapFactory.Options();
363         options.inJustDecodeBounds = true;
364         BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
365 
366         // Calculate inSampleSize
367         options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
368 
369         // Decode bitmap with inSampleSize set
370         options.inJustDecodeBounds = false;
371         return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
372     }
373 
374     /**
375      * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
376      * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates
377      * the closest inSampleSize that will result in the final decoded bitmap having a width and
378      * height equal to or larger than the requested width and height. This implementation does not
379      * ensure a power of 2 is returned for inSampleSize which can be faster when decoding but
380      * results in a larger bitmap which isn't as useful for caching purposes.
381      *
382      * @param options An options object with out* params already populated (run through a decode*
383      *            method with inJustDecodeBounds==true
384      * @param reqWidth The requested width of the resulting bitmap
385      * @param reqHeight The requested height of the resulting bitmap
386      * @return The value to be used for inSampleSize
387      */
calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight)388     public static int calculateInSampleSize(BitmapFactory.Options options,
389             int reqWidth, int reqHeight) {
390         // Raw height and width of image
391         final int height = options.outHeight;
392         final int width = options.outWidth;
393         int inSampleSize = 1;
394 
395         if (height > reqHeight || width > reqWidth) {
396 
397             // Calculate ratios of height and width to requested height and width
398             final int heightRatio = Math.round((float) height / (float) reqHeight);
399             final int widthRatio = Math.round((float) width / (float) reqWidth);
400 
401             // Choose the smallest ratio as inSampleSize value, this will guarantee a final image
402             // with both dimensions larger than or equal to the requested height and width.
403             inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
404 
405             // This offers some additional logic in case the image has a strange
406             // aspect ratio. For example, a panorama may have a much larger
407             // width than height. In these cases the total pixels might still
408             // end up being too large to fit comfortably in memory, so we should
409             // be more aggressive with sample down the image (=larger inSampleSize).
410 
411             final float totalPixels = width * height;
412 
413             // Anything more than 2x the requested pixels we'll sample down further
414             final float totalReqPixelsCap = reqWidth * reqHeight * 2;
415 
416             while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
417                 inSampleSize++;
418             }
419         }
420         return inSampleSize;
421     }
422 }
423