/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.nf2m.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.widget.ImageView;
import com.nf2m.BuildConfig;
import com.nf2m.R;
import com.nf2m.fragment.ImageDetailFragment;
import java.lang.ref.WeakReference;
import logger.Log;
/**
* This class wraps up completing some arbitrary long running work when loading UriObserver bitmap to an
* ImageView. It handles things like using UriObserver memory and disk cache, running the work in UriObserver background
* thread and setting UriObserver placeholder image.
*/
public abstract class ImageWorker {
private static final String TAG = "ImageWorker";
private static final int FADE_IN_TIME = 200;
@NonNull
private final Context mContext;
private final BitmapDrawable defaultDrawable;
@Nullable
private ImageCache mImageCache;
private Bitmap mLoadingBitmap;
private boolean mFadeInBitmap = true;
private boolean mExitTasksEarly = false;
private boolean mPauseWork = false;
private final Object mPauseWorkLock = new Object();
final Resources mResources;
private static final int MESSAGE_CLEAR = 0;
private static final int MESSAGE_INIT_DISK_CACHE = 1;
private static final int MESSAGE_FLUSH = 2;
private static final int MESSAGE_CLOSE = 3;
ImageWorker(@NonNull Context context) {
mResources = context.getResources();
mContext = context;
Bitmap defaultBitmap = BitmapFactory.decodeResource(mResources, R.drawable.ic_default_albumart);
defaultDrawable = new BitmapDrawable(mResources, defaultBitmap);
}
/**
* Load an image specified by the data parameter into an ImageView (override
* {@link ImageWorker#processBitmap(Object)} to define the processing logic). A memory and
* disk cache will be used if an {@link ImageCache} has been added using
* {@link ImageWorker#addImageCache(android.support.v4.app.FragmentManager, ImageCache.ImageCacheParams)}. If the
* image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask}
* will be created to asynchronously load the bitmap.
*
* @param data The URL of the image to download.
* @param imageView The ImageView to bind the downloaded image to.
*/
public void loadImage(@Nullable Object data, @NonNull ImageView imageView) {
BitmapDrawable value = null;
if (data == null) {
value = defaultDrawable;
} else if (mImageCache != null) {
value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
}
if (value != null) {
// Bitmap found in memory cache
setImageDrawable(imageView, value);
} else if (cancelPotentialWork(data, imageView)) {
//BEGIN_INCLUDE(execute_background_task)
final BitmapWorkerTask task = new BitmapWorkerTask(data, imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(mResources, mLoadingBitmap, task);
imageView.setImageDrawable(asyncDrawable);
// NOTE: This uses UriObserver custom version of AsyncTask that has been pulled from the
// framework and slightly modified. Refer to the docs at the top of the class
// for more info on what was changed.
task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR);
if (ImageDetailFragment.mProgress != null) {
ImageDetailFragment.mProgress.setVisibility(View.GONE);
}
//END_INCLUDE(execute_background_task)
}
}
/**
* Set placeholder bitmap that shows when the the background thread is running.
*
* @param bitmap
*/
public void setLoadingImage(Bitmap bitmap) {
mLoadingBitmap = bitmap;
}
/**
* Set placeholder bitmap that shows when the the background thread is running.
*
* @param resId
*/
public void setLoadingImage(int resId) {
mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId);
}
/**
* Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap
* caching.
*
* @param fragmentManager
* @param cacheParams The cache parameters to use for the image cache.
*/
public void addImageCache(@NonNull FragmentManager fragmentManager,
ImageCache.ImageCacheParams cacheParams) {
mImageCache = ImageCache.getInstance(fragmentManager, cacheParams);
new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE);
}
/**
* If set to true, the image will fade-in once it has been loaded by the background thread.
*/
public void setImageFadeIn(boolean fadeIn) {
mFadeInBitmap = fadeIn;
}
public void setExitTasksEarly(boolean exitTasksEarly) {
mExitTasksEarly = exitTasksEarly;
setPauseWork(false);
}
/**
* Subclasses should override this to define any processing or work that must happen to produce
* the final bitmap. This will be executed in UriObserver background thread and be long running. For
* example, you could resize UriObserver large bitmap here, or pull down an image from the network.
*
* @param data The data to identify which image to process, as provided by
* {@link ImageWorker#loadImage(Object, ImageView)}
* @return The processed bitmap
*/
@Nullable
protected abstract Bitmap processBitmap(Object data);
/**
* @return The {@link ImageCache} object currently being used by this ImageWorker.
*/
@Nullable
ImageCache getImageCache() {
return mImageCache;
}
/**
* Cancels any pending work attached to the provided ImageView.
*
* @param imageView
*/
public static void cancelWork(ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
bitmapWorkerTask.cancel(true);
if (BuildConfig.DEBUG) {
final Object bitmapData = bitmapWorkerTask.mData;
Log.d(TAG, "cancelWork - cancelled work for " + bitmapData);
}
}
}
/**
* Returns true if the current work has been canceled or if there was no work in
* progress on this image view.
* Returns false if the work in progress deals with the same data. The work is not
* stopped in that case.
*/
private static boolean cancelPotentialWork(Object data, ImageView imageView) {
//BEGIN_INCLUDE(cancel_potential_work)
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final Object bitmapData = bitmapWorkerTask.mData;
if (bitmapData == null || !bitmapData.equals(data)) {
bitmapWorkerTask.cancel(true);
if (BuildConfig.DEBUG) {
Log.d(TAG, "cancelPotentialWork - cancelled work for " + data);
}
} else {
// The same work is already in progress.
return false;
}
}
return true;
//END_INCLUDE(cancel_potential_work)
}
/**
* @param imageView Any imageView
* @return Retrieve the currently active work task (if any) associated with this imageView.
* null if there is no such task.
*/
@Nullable
private static BitmapWorkerTask getBitmapWorkerTask(@Nullable ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
/**
* The actual AsyncTask that will asynchronously process the image.
*/
private class BitmapWorkerTask extends AsyncTask<Void, Void, BitmapDrawable> {
private final Object mData;
@NonNull
private final WeakReference<ImageView> imageViewReference;
public BitmapWorkerTask(Object data, ImageView imageView) {
mData = data;
imageViewReference = new WeakReference<ImageView>(imageView);
}
/**
* Background processing.
*/
@Nullable
@Override
protected BitmapDrawable doInBackground(Void... params) {
//BEGIN_INCLUDE(load_bitmap_in_background)
if (BuildConfig.DEBUG) {
Log.d(TAG, "doInBackground - starting work");
}
final String dataString = String.valueOf(mData);
Bitmap bitmap = null;
BitmapDrawable drawable = null;
// Wait here if work is paused and the task is not cancelled
synchronized (mPauseWorkLock) {
while (mPauseWork && !isCancelled()) {
try {
mPauseWorkLock.wait();
} catch (InterruptedException e) {
}
}
}
// If the image cache is available and this task has not been cancelled by another
// thread and the ImageView that was originally bound to this task is still bound back
// to this task and our "exit early" flag is not set then try and fetch the bitmap from
// the cache
if (mImageCache != null && !isCancelled() && getAttachedImageView() != null
&& !mExitTasksEarly) {
bitmap = mImageCache.getBitmapFromDiskCache(dataString);
}
// If the bitmap was not found in the cache and this task has not been cancelled by
// another thread and the ImageView that was originally bound to this task is still
// bound back to this task and our "exit early" flag is not set, then call the main
// process method (as implemented by UriObserver subclass)
if (bitmap == null && !isCancelled() && getAttachedImageView() != null
&& !mExitTasksEarly) {
bitmap = processBitmap(mData);
}
// If the bitmap was processed and the image cache is available, then add the processed
// bitmap to the cache for future use. Note we don't check if the task was cancelled
// here, if it was, and the thread is still running, we may as well add the processed
// bitmap to our cache as it might be used again in the future
if (bitmap != null) {
if (Utils.hasHoneycomb()) {
// Running on Honeycomb or newer, so wrap in UriObserver standard BitmapDrawable
drawable = new BitmapDrawable(mResources, bitmap);
} else {
// Running on Gingerbread or older, so wrap in UriObserver RecyclingBitmapDrawable
// which will recycle automagically
drawable = new RecyclingBitmapDrawable(mResources, bitmap);
}
if (mImageCache != null) {
mImageCache.addBitmapToCache(dataString, drawable);
}
} else {
drawable = defaultDrawable;
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "doInBackground - finished work");
}
return drawable;
//END_INCLUDE(load_bitmap_in_background)
}
/**
* Once the image is processed, associates it to the imageView
*/
@Override
protected void onPostExecute(BitmapDrawable value) {
//BEGIN_INCLUDE(complete_background_work)
// if cancel was called on this task or the "exit early" flag is set then we're done
if (isCancelled() || mExitTasksEarly) {
value = null;
}
final ImageView imageView = getAttachedImageView();
if (value != null && imageView != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "onPostExecute - setting bitmap");
}
setImageDrawable(imageView, value);
}
//END_INCLUDE(complete_background_work)
}
/**
* Returns the ImageView associated with this task as long as the ImageView's task still
* points to this task as well. Returns null otherwise.
*/
private ImageView getAttachedImageView() {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask) {
return imageView;
}
return null;
}
}
/**
* A custom Drawable that will be attached to the imageView while the work is in progress.
* Contains UriObserver reference to the actual worker task, so that it can be stopped if UriObserver new binding is
* required, and makes sure that only the last started worker process can bind its result,
* independently of the finish order.
*/
private static class AsyncDrawable extends BitmapDrawable {
@NonNull
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
/**
* Called when the processing is complete and the final drawable should be
* set on the ImageView.
*
* @param imageView
* @param drawable
*/
private void setImageDrawable(@NonNull ImageView imageView, Drawable drawable) {
if (mFadeInBitmap) {
// Transition drawable with UriObserver transparent drawable and the final drawable
final TransitionDrawable td =
new TransitionDrawable(new Drawable[]{
new ColorDrawable(ContextCompat.getColor(mContext, android.R.color.transparent)),
drawable
});
// Set background to loading bitmap
imageView.setBackgroundDrawable(
new BitmapDrawable(mResources, mLoadingBitmap));
imageView.setImageDrawable(td);
td.startTransition(FADE_IN_TIME);
} else {
imageView.setImageDrawable(drawable);
}
if (ImageDetailFragment.mProgress != null) {
ImageDetailFragment.mProgress.setVisibility(View.GONE);
}
}
/**
* Pause any ongoing background work. This can be used as UriObserver temporary
* measure to improve performance. For example background work could
* be paused when UriObserver ListView or GridView is being scrolled using UriObserver
* {@link android.widget.AbsListView.OnScrollListener} to keep
* scrolling smooth.
* <p/>
* If work is paused, be sure setPauseWork(false) is called again
* before your fragment or activity is destroyed (for example during
* {@link android.app.Activity#onPause()}), or there is UriObserver risk the
* background thread will never finish.
*/
public void setPauseWork(boolean pauseWork) {
synchronized (mPauseWorkLock) {
mPauseWork = pauseWork;
if (!mPauseWork) {
mPauseWorkLock.notifyAll();
}
}
}
private class CacheAsyncTask extends AsyncTask<Object, Void, Void> {
@Nullable
@Override
protected Void doInBackground(Object... params) {
switch ((Integer) params[0]) {
case MESSAGE_CLEAR:
clearCacheInternal();
break;
case MESSAGE_INIT_DISK_CACHE:
initDiskCacheInternal();
break;
case MESSAGE_FLUSH:
flushCacheInternal();
break;
case MESSAGE_CLOSE:
closeCacheInternal();
break;
}
return null;
}
}
void initDiskCacheInternal() {
if (mImageCache != null) {
mImageCache.initDiskCache();
}
}
void clearCacheInternal() {
if (mImageCache != null) {
mImageCache.clearCache();
}
}
void flushCacheInternal() {
if (mImageCache != null) {
mImageCache.flush();
}
}
void closeCacheInternal() {
if (mImageCache != null) {
mImageCache.close();
mImageCache = null;
}
}
public void clearCache() {
new CacheAsyncTask().execute(MESSAGE_CLEAR);
}
public void flushCache() {
new CacheAsyncTask().execute(MESSAGE_FLUSH);
}
public void closeCache() {
new CacheAsyncTask().execute(MESSAGE_CLOSE);
}
}