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
17package com.example.android.contactslist.util;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.graphics.Bitmap;
22import android.graphics.BitmapFactory;
23import android.graphics.drawable.BitmapDrawable;
24import android.graphics.drawable.ColorDrawable;
25import android.graphics.drawable.Drawable;
26import android.graphics.drawable.TransitionDrawable;
27import android.os.AsyncTask;
28import android.support.v4.app.FragmentManager;
29import android.util.Log;
30import android.widget.ImageView;
31
32import com.example.android.contactslist.BuildConfig;
33
34import java.io.FileDescriptor;
35import 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 */
42public 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
54    protected ImageLoader(Context context, int imageSize) {
55        mResources = context.getResources();
56        mImageSize = imageSize;
57    }
58
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     */
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     */
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     */
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     */
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     */
132    protected abstract Bitmap processBitmap(Object data);
133
134    /**
135     * Cancels any pending work attached to the provided ImageView.
136     */
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     */
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     */
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
195        public BitmapWorkerTask(ImageView imageView) {
196            imageViewReference = new WeakReference<ImageView>(imageView);
197        }
198
199        /**
200         * Background processing.
201         */
202        @Override
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
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
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         */
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
295        public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
296            super(res, bitmap);
297            bitmapWorkerTaskReference =
298                new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
299        }
300
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     */
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     */
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     */
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     */
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