/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License (the "License").
* You may not use this file except in compliance with the License.
*
* You can obtain a copy of the license at
* src/com/vodafone360/people/VODAFONE.LICENSE.txt or
* http://github.com/360/360-Engine-for-Android
* See the License for the specific language governing permissions and
* limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each file and
* include the License file at src/com/vodafone360/people/VODAFONE.LICENSE.txt.
* If applicable, add the following below this CDDL HEADER, with the fields
* enclosed by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
* Copyright 2010 Vodafone Sales & Services Ltd. All rights reserved.
* Use is subject to license terms.
*/
package com.vodafone360.people;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.view.View;
import android.widget.ImageView;
import com.vodafone360.people.utils.LRUHashMap;
import com.vodafone360.people.utils.LogUtils;
import com.vodafone360.people.utils.ThumbnailUtils;
/***
* Unified thumbnail cache with asynchronous file reading, intended to be
* utilised by multiple activities.
*/
public class ThumbnailCache {
/** Thread name. **/
private static final String THREAD_NAME = "ThumbnailCacheThread";
/** Request queue size (number of possible on screen items). **/
private static final int REQUEST_QUEUE_SIZE = 9;
/**
* Thumbnails cache size (REQUEST_QUEUE_SIZE or greater, but larger values
* are OK due to garbage collection).
*/
private static final int THUMBNAILS_CACHE_SIZE = 20;
/**
* Keep the Thumbnail Cache thread in the background by making it wait a
* tiny bit between loading files.
*/
private static final long THREAD_WAIT = 5L;
/** Thumbnail cache. **/
private final LRUHashMap<Long, SoftReference<Bitmap>> mThumbnailCache;
/** Request queue (must be synchronised). **/
private final List<Item> mRequestQueue;
/** List of invalid Thumbnails. **/
private final List<Long> mInvalidatedThumbnails;
/**
* Instance of the background thread (must always be accessed by the main
* thread only).
*/
private BackgroundThread mBackgroundThread;
/** Background Thread sync object. **/
private Object mBackgroundThreadSync = new Object();
/** Reference to activity which is currently using this item. **/
private Activity mActivity;
/** True if the background thread should not be doing any work. **/
private boolean mPaused = false;
/** Item in the request queue. **/
private class Item {
/** Contact ID. **/
private final long mContactId;
/** ImageView to populate. **/
private final ImageView mImageView;
/***
* Item constructor.
*
* @param contactId Contact ID.
* @param imageView ImageView to populate.
*/
public Item(final long contactId, final ImageView imageView) {
mContactId = contactId;
mImageView = imageView;
}
/***
* Contact ID.
*
* @return Contact ID.
*/
public long getContactId() {
return mContactId;
}
/***
* ImageView to populate.
*
* @return ImageView to populate.
*/
public ImageView getImageView() {
return mImageView;
}
}
/***
* Create the Cache.
*/
public ThumbnailCache() {
mThumbnailCache = new LRUHashMap<Long, SoftReference<Bitmap>>(THUMBNAILS_CACHE_SIZE);
mRequestQueue = new ArrayList<Item>(REQUEST_QUEUE_SIZE);
mInvalidatedThumbnails = new ArrayList<Long>();
}
/***
* Subscribe this Activity to the cache.
*
* @param activity Current Activity for posting to a UI thread.
*/
public final void subscribe(final Activity activity) {
synchronized (mBackgroundThreadSync) {
if (mBackgroundThread == null) {
mBackgroundThread = new BackgroundThread();
}
}
mActivity = activity;
pauseThread(false);
}
/***
* Removed the background thread and clear all pending work.
*/
public final void unsubscribe() {
synchronized (mBackgroundThreadSync) {
if (mBackgroundThread != null) {
mBackgroundThread.killThread();
}
mBackgroundThread = null;
}
mPaused = true;
mActivity = null;
synchronized (mRequestQueue) {
mRequestQueue.clear();
}
}
/***
* Pause the Background Thread, while keeping the work queue.
*
* @param pause TRUE if the thread should be paused, FALSE will immediately
* resume the thread.
*/
public final void pauseThread(final boolean pause) {
mPaused = pause;
synchronized (mBackgroundThreadSync) {
if (!pause && mBackgroundThread != null) {
mBackgroundThread.doWork();
}
}
}
/***
* Sets the given ImageView with the cached thumbnail for this contact ID,
* using the default thumbnail if it is not cached. If the thumbnail is
* not cached and "queue" is set to TRUE, then a background thread will try
* and populate the ImageView later.
*
* @param imageView ImageView to set now (from cache) or later (in
* background thread).
* @param localContactId ID of the contact thumbnail.
* @param defaultThumbnailId ID of default thumbnail resource.
*/
public final void setThumbnail(final ImageView imageView,
final long localContactId, final int defaultThumbnailId) {
if (imageView == null) {
throw new InvalidParameterException("ThumbnailCache.setThumbnail() "
+ "ImageView should not be NULL");
}
boolean imageSet = false;
/** Associate the ImageView with the contact ID. **/
imageView.setTag(localContactId);
/** Check the thumbnail cache. **/
final SoftReference<Bitmap> bitmapRef
= mThumbnailCache.get(localContactId);
if (bitmapRef != null) {
final Bitmap thumbnail = bitmapRef.get();
if (thumbnail != null) {
/** Instantly return a cached reference. **/
imageView.setImageBitmap(thumbnail);
imageSet = true;
if (!mInvalidatedThumbnails.contains(localContactId)) {
/** This image is valid, so don't try and reload. **/
return;
}
} else {
/** Remove any faulty references from the cache. **/
LogUtils.logW("ThumbnailCache.setThumbnail() "
+ "Bad reference removed id[" + localContactId + "]");
mThumbnailCache.remove(localContactId);
}
}
/** Add this thumbnail to the request queue. **/
synchronized (mRequestQueue) {
if (!mRequestQueue.contains(localContactId)) {
/** Remove oldest item in the list. **/
if (mRequestQueue.size() > REQUEST_QUEUE_SIZE) {
mRequestQueue.remove(0);
}
mRequestQueue.add(new Item(localContactId, imageView));
synchronized (mBackgroundThreadSync) {
if (mBackgroundThread != null) {
mBackgroundThread.doWork();
}
}
}
}
if (!imageSet) {
/** Thumbnail was not found, so use default. **/
imageView.setImageResource(defaultThumbnailId);
}
}
/***
* Clear the entire thumbnail cache to save memory.
*/
private final void clearThumbnailCache() {
synchronized (mRequestQueue) {
mRequestQueue.clear();
}
mThumbnailCache.clear();
mInvalidatedThumbnails.clear();
System.gc();
}
/***
* Invalidate the entire thumbnail cache, that way a thumbnail that has
* been changed in the file system will be updated the next time it is
* requested by the UI.
*/
public final void invalidateThumbnailCache() {
mInvalidatedThumbnails.clear();
synchronized (mThumbnailCache) {
for (Long contactId : mThumbnailCache.keySet()) {
mInvalidatedThumbnails.add(contactId);
}
}
}
/***
* Perform work in a background thread.
*/
private class BackgroundThread extends Thread {
/**
* Object is used to indicate that this thread has started, and for
* pausing and notifying the Thread. Thread will wait on this object.
*/
private Object mRunning;
/** Set to TRUE to indicate that this thread should roll to a stop. **/
private boolean mKillThread = false;
/***
* Start or bring the thread out of wait state to do some background
* work.
*/
public void doWork() {
if (mKillThread) {
/** Exit now. **/
return;
} else if (mRunning == null) {
/** First time start. **/
mRunning = new Object();
start();
} else if (mRequestQueue.size() != 0 && !mPaused) {
/** Notify the running thread. **/
synchronized (mRunning) {
mRunning.notify();
}
}
}
@Override
public void run() {
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_BACKGROUND);
Thread.currentThread().setName(THREAD_NAME);
while (!mKillThread) {
loadThumbnails();
threadWait(null);
}
}
/***
* Load thumbnails from the file IO, posting a cache updated
* notification to the handled as long as one file has been loaded.
*/
private final void loadThumbnails() {
while (!mKillThread) {
if (mPaused || mActivity == null) {
return;
}
final Item item;
synchronized (mRequestQueue) {
if (mRequestQueue.size() < 1) {
/** Loading done. **/
return;
}
item = mRequestQueue.remove(0);
}
if (item.getContactId()
!= getLocalContactId(item.getImageView())) {
/** ImageView has been Recycled. **/
continue;
}
/**
* Do all expensive File IO and Bitmap decoding work.
*/
final String path = ThumbnailUtils.thumbnailPath(
item.getContactId());
if (path != null) {
try {
/* Using reflection to set inPurgeable flag as it is not available on 1.5 */
Class bitmapFactoryOptionsClass = BitmapFactory.Options.class;
BitmapFactory.Options bitmapFactoryOptionsInstance = new BitmapFactory.Options();
Field field;
try {
field = bitmapFactoryOptionsClass.getField("inPurgeable");
field.setBoolean(bitmapFactoryOptionsInstance, true);
} catch (SecurityException e) {
LogUtils.logW("ThumbnailCache.loadThumbnails() "
+ "Security Exception");
} catch (NoSuchFieldException e) {
LogUtils.logW("ThumbnailCache.loadThumbnails() "
+ "Field not found");
} catch (IllegalArgumentException e) {
LogUtils.logW("ThumbnailCache.loadThumbnails() "
+ "Illegal Argument");
} catch (IllegalAccessException e) {
LogUtils.logW("ThumbnailCache.loadThumbnails() "
+ "Illegal Access");
}
final Bitmap bitmap = BitmapFactory.decodeFile(path, bitmapFactoryOptionsInstance);
mThumbnailCache.put(item.getContactId(),
new SoftReference<Bitmap>(bitmap));
/** Thumbnail is now valid. **/
mInvalidatedThumbnails.remove(item.getContactId());
if (item.getContactId()
!= getLocalContactId(item.getImageView())) {
/** ImageView has been Recycled. **/
continue;
}
if (mActivity == null) {
return;
}
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
/**
* Check if the view is still the same, and
* hasn't been re-used by the ListView.
*/
if (item.getContactId()
== getLocalContactId(item.getImageView())) {
item.getImageView().setImageBitmap(bitmap);
}
}});
} catch (NullPointerException e) {
LogUtils.logE("ThumbnailCache.loadThumbnails() "
+ "Unexpected NullPointerException while "
+ "loading thumbnails, clearing Thumbnail "
+ "cache for safety.", e);
clearThumbnailCache();
} catch (OutOfMemoryError outOfMemoryError) {
LogUtils.logE("ThumbnailCache.loadThumbnails() "
+ "Low on memory while decoding thumbnails",
outOfMemoryError);
clearThumbnailCache();
}
}
threadWait(THREAD_WAIT);
}
}
/***
* Stop the thread between cycles to stop it overloading the device and
* possibly blocking the UI.
*
* @param wait Time to wait in milliseconds, or NULL to wait for a
* thread notify.
*/
private void threadWait(final Long wait) {
synchronized (mRunning) {
try {
if (mKillThread) {
/** Never wait while thread is being killed. **/
return;
} else if (wait == null) {
mRunning.wait();
} else {
mRunning.wait(wait);
}
} catch (InterruptedException e) {
// Do nothing.
}
}
}
/***
* Kills the running background thread.
*/
public void killThread() {
mKillThread = true;
synchronized (mRunning) {
mRunning.notify();
}
}
}
/***
* Return the local contact ID tag for the given View.
*
* @param view View to extract the list position information.
* @return List position of the view.
*/
private static long getLocalContactId(final View view) {
final Object localContactIdObject = view.getTag();
if (localContactIdObject == null) {
LogUtils.logW("ThumbnailCache.getLocalContactId() "
+ "ID for view should not be NULL");
return -1L;
} else {
return (Long) localContactIdObject;
}
}
/***
* Return TRUE if the background thread is paused.
*
* @return TRUE if the background thread is paused.
*/
public boolean isPaused() {
return mPaused;
}
}