package vandy.mooc.utils.loader;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import vandy.mooc.common.GenericAsyncTask;
import vandy.mooc.common.GenericAsyncTaskOps;
import vandy.mooc.utils.BitmapUtils;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
/**
* This class loads and displays images in the background. It maintains
* a cache of bitmaps to make displaying the same image multiple times
* more efficient.
*/
public class ImageLoader
implements GenericAsyncTaskOps<ImageLoaderWorkOrder,
Void,
ImageLoaderWorkResult>{
/**
* Logcat Tag
*/
public static final String TAG =
ImageLoader.class.getCanonicalName();
/**
* Executor used to load the images from disk in the background.
*/
private final Executor mDisplayThreadPoolExecutor =
ImageLoaderThreadPool.MY_THREAD_POOL_EXECUTOR;
/**
* Map of ImageView's (using their hash codes) to the filepath of
* the image they are currently displaying. This is used to ensure
* that the ImageView is displaying the image that is being loaded
* for it.
*/
private final Map<Integer, String> mCacheKeysForImageView =
new ConcurrentHashMap<>();
/**
* Map storing ReentrantLocks for each file.
*/
private final Map<String, ReentrantLock> fileLocks =
new WeakHashMap<String, ReentrantLock>();
/**
* Drawable that is displayed while the image is loading
*/
private final Drawable mLoadingDrawable;
/**
* Cache storing the bitmaps in memory.
*/
private LruCache<String, Bitmap> mBitmapCache;
/**
* Constructor initializes the fields.
*/
public ImageLoader(Drawable loadingDrawable) {
mLoadingDrawable = loadingDrawable;
initCache();
}
/**
* Initialize the image cache.
*/
private void initCache() {
// Maximum memory allowed for this application in Mb.
final int maxMemory =
(int) (Runtime.getRuntime().maxMemory() / 1024);
// Maximum size of the cache - set to 1/4th of the memory
// allowed for this application.
final int cacheSize = maxMemory / 4;
// Create the cache using the cache size above
mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
// Returns the size of an item in the cache.
@Override
protected int sizeOf(String key,
Bitmap value) {
return value.getAllocationByteCount() / 1024;
}
};
}
/**
* Adds the bitmap to the image cache.
*/
private void addBitmapToCache(String filename,
Bitmap bitmap) {
Log.d(TAG, "added BM to cache for position: " + filename);
mBitmapCache.put(filename,
bitmap);
}
/**
* Returns the ReentrantLock associated with the
* file path parameter.
*/
private ReentrantLock getFileLock(String filepath) {
ReentrantLock lock =
fileLocks.get(filepath);
if (lock == null) {
lock = new ReentrantLock();
fileLocks.put(filepath,
lock);
}
return lock;
}
/**
* Sets the ImageView parameter to the bitmap at the filepath
* parameter by loading it in a background thread. A cache is
* maintained to make loading bitmaps multiple times more
* efficient.
*/
public void loadAndDisplayImage(ImageView view,
String imageFilePath,
int colWidth) {
// Store the holder and its current filepath in the cache
mCacheKeysForImageView.put(view.hashCode(),
imageFilePath);
Bitmap cachedBitmap =
mBitmapCache.get(imageFilePath);
if (cachedBitmap != null)
// If the bitmap is in the cache, simply place it in the
// ImageView.
view.setImageBitmap(cachedBitmap);
else {
// If the bitmap isn't in the cache, set the ImageView to
// display the loading drawable
view.setImageDrawable(mLoadingDrawable);
// Create the work order that contains the info
// needed to load and display the image.
ImageLoaderWorkOrder wo =
new ImageLoaderWorkOrder(new ImageViewHolder(view),
imageFilePath,
colWidth,
colWidth);
// Create a new GenericAsyncTask
GenericAsyncTask<ImageLoaderWorkOrder,
Void,
ImageLoaderWorkResult,
ImageLoader> imageLoaderTask =
new GenericAsyncTask<> (this);
// Use the task to load the image in the background.
imageLoaderTask
.executeOnExecutor(mDisplayThreadPoolExecutor,
wo);
}
}
/**
* Checks if the ImageView wrapped by the ImageViewHolder has
* been garbage collected or reused to display a different
* image. To ensure that the wrapped view is not GC'd while
* this check is being performed, we first need to grab a
* reference to that view.
*/
private void checkImageView(ImageViewHolder imgView,
String filepath)
throws ViewChangedException {
// Grab a reference to the wrapped view so that
// it won't be GC'd between the two checks below.
ImageView view = imgView.getWrappedImageView();
checkViewCollected(imgView);
checkViewReused(imgView,
filepath);
}
/**
* Checks if the view has been collected by
* the garbage collector
*/
private void checkViewCollected(ImageViewHolder imgView)
throws ViewChangedException {
if (imgView.isCollected())
throw new ViewChangedException();
}
/**
* Checks if the view has been reused to display
* a different image.
*/
private void checkViewReused(ImageViewHolder imgView,
String filepath)
throws ViewChangedException {
if (isViewReused(imgView, filepath))
throw new ViewChangedException();
}
/**
* Checks if the view has been reused without
* throwing an exception. If the wrapped image
* view has been garbage collected, this method
* will return true.
*/
private boolean isViewReused(ImageViewHolder imgView,
String filepath) {
// Get a reference to the wrapped view to prevent
// garbage collection.
ImageView view = imgView.getWrappedImageView();
if (view == null) {
// Must have been collected.
return true;
}
final String currCachedKey =
mCacheKeysForImageView.get(view.hashCode());
return currCachedKey != filepath;
}
/**
* The Exception that is used to cancel the
* loading if the view has been collected or
* reused.
*/
@SuppressWarnings("serial")
class ViewChangedException extends Exception {}
/**
* Hook method called by the GenericAsyncTask framework
* to perform the background processing. It loads the image
* from disk while periodically checking if the view has been
* reused.
*/
@Override
public ImageLoaderWorkResult doInBackground(ImageLoaderWorkOrder... param) {
ImageLoaderWorkOrder wo = param[0];
// Obtain a lock on this image file.
ReentrantLock lock = getFileLock(wo.getmFilePath());
lock.lock();
Bitmap result = null;
try {
// Retrieve data from the work order
ImageViewHolder holder = wo.getmImageViewHolder();
String filepath = wo.getmFilePath();
int width = wo.getmTargetWidth();
int height = wo.getmTargetHeight();
// Check that the view is still valid
checkImageView(holder, filepath);
// Load the bitmap
result =
BitmapUtils
.decodeSampledBitmapFromFile(filepath,
width,
height);
// re-check the view's validity
checkImageView(holder, filepath);
} catch (ViewChangedException e) {
// Caught if the view is no longer valid.
// Halt loading the image.
return null;
} finally {
lock.unlock();
}
return new ImageLoaderWorkResult
(wo.getmImageViewHolder(),
wo.getmFilePath(),
result);
}
/**
* Hook method called by the GenericAsyncTask framework when the
* background processing has completed. This will check that the
* ImageView is still set to display the loaded bitmap, and display
* it if so.
*/
@Override
public void onPostExecute(ImageLoaderWorkResult result) {
if (result != null) {
ImageViewHolder holder = result.getmImageViewHolder();
String filepath = result.getmFilePath();
// Check that the ImageView is still valid
if (!holder.isCollected()
&& !isViewReused(holder, filepath)) {
addBitmapToCache(filepath,
result.getmBitmap());
// Display the loaded bitmap
holder.getWrappedImageView()
.setImageBitmap(result.getmBitmap());
}
}
}
}