--- /dev/null
+package org.openintents.filemanager;
+
+import java.lang.ref.SoftReference;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.openintents.filemanager.util.FileUtils;
+import org.openintents.filemanager.util.ImageUtils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.Log;
+import android.widget.ImageView;
+
+public class ThumbnailLoader {
+
+ private static final String TAG = "OIFM_ThumbnailLoader";
+
+ // Both hard and soft caches are purged after 40 seconds idling.
+ private static final int DELAY_BEFORE_PURGE = 40000;
+ private static final int MAX_CACHE_CAPACITY = 40;
+
+ // Maximum number of threads in the executor pool.
+ // TODO: Tune POOL_SIZE for maximum performance gain
+ private static final int POOL_SIZE = 5;
+
+ private boolean cancel;
+ private Context mContext;
+
+ //private static int thumbnailWidth = 96;
+ //private static int thumbnailHeight = 129;
+ private static int thumbnailWidth = 32;
+ private static int thumbnailHeight = 32;
+
+ private Runnable purger;
+ private Handler purgeHandler;
+ private ExecutorService mExecutor;
+
+ // Soft bitmap cache for thumbnails removed from the hard cache.
+ // This gets cleared by the Garbage Collector everytime we get low on memory.
+ private ConcurrentHashMap<String, SoftReference<Bitmap>> mSoftBitmapCache;
+ private LinkedHashMap<String, Bitmap> mHardBitmapCache;
+ private ArrayList<String> mBlacklist;
+
+ /**
+ * Used for loading and decoding thumbnails from files.
+ *
+ * @author PhilipHayes
+ * @param context Current application context.
+ */
+ public ThumbnailLoader(Context context) {
+ mContext = context;
+
+ purger = new Runnable(){
+ @Override
+ public void run() {
+ Log.d(TAG, "Purge Timer hit; Clearing Caches.");
+ clearCaches();
+ }
+ };
+
+ purgeHandler = new Handler();
+ mExecutor = Executors.newFixedThreadPool(POOL_SIZE);
+
+ mBlacklist = new ArrayList<String>();
+ mSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(MAX_CACHE_CAPACITY / 2);
+ mHardBitmapCache = new LinkedHashMap<String, Bitmap>(MAX_CACHE_CAPACITY / 2, 0.75f, true){
+
+ /***/
+ private static final long serialVersionUID = 1347795807259717646L;
+
+ @Override
+ protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest){
+ // Moves the last used item in the hard cache to the soft cache.
+ if(size() > MAX_CACHE_CAPACITY){
+ mSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+ }
+
+ public static void setThumbnailHeight(int height) {
+ thumbnailHeight = height;
+ thumbnailWidth = height * 4 / 3;
+ }
+
+ /**
+ *
+ * @param parentFile The current directory.
+ * @param text The IconifiedText container.
+ * @param imageView The ImageView from the IconifiedTextView.
+ */
+ public void loadImage(String parentFile, IconifiedText text, ImageView imageView) {
+ if(!cancel && !mBlacklist.contains(text.getText())){
+ // We reset the caches after every 30 or so seconds of inactivity for memory efficiency.
+ resetPurgeTimer();
+
+ Bitmap bitmap = getBitmapFromCache(text.getText());
+ if(bitmap != null){
+ // We're still in the UI thread so we just update the icons from here.
+ imageView.setImageBitmap(bitmap);
+ text.setIcon(bitmap);
+ } else {
+ if (!cancel) {
+ // Submit the file for decoding.
+ Thumbnail thumbnail = new Thumbnail(parentFile, imageView, text);
+ WeakReference<ThumbnailRunner> runner = new WeakReference<ThumbnailRunner>(new ThumbnailRunner(thumbnail));
+ mExecutor.submit(runner.get());
+ }
+ }
+ }
+ }
+ /**
+ * Cancels any downloads, shuts down the executor pool,
+ * and then purges the caches.
+ */
+ public void cancel(){
+ cancel = true;
+
+ // We could also terminate it immediately,
+ // but that may lead to synchronization issues.
+ if(!mExecutor.isShutdown()){
+ mExecutor.shutdown();
+ }
+
+ stopPurgeTimer();
+
+ mContext = null;
+ clearCaches();
+ }
+
+ /**
+ * Stops the cache purger from running until it is reset again.
+ */
+ public void stopPurgeTimer(){
+ purgeHandler.removeCallbacks(purger);
+ }
+
+ /**
+ * Purges the cache every (DELAY_BEFORE_PURGE) milliseconds.
+ * @see DELAY_BEFORE_PURGE
+ */
+ private void resetPurgeTimer() {
+ purgeHandler.removeCallbacks(purger);
+ purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE);
+ }
+
+ private void clearCaches(){
+ mSoftBitmapCache.clear();
+ mHardBitmapCache.clear();
+ mBlacklist.clear();
+ }
+
+ /**
+ * @param key In this case the file name (used as the mapping id).
+ * @return bitmap The cached bitmap or null if it could not be located.
+ *
+ * As the name suggests, this method attemps to obtain a bitmap stored
+ * in one of the caches. First it checks the hard cache for the key.
+ * If a key is found, it moves the cached bitmap to the head of the cache
+ * so it gets moved to the soft cache last.
+ *
+ * If the hard cache doesn't contain the bitmap, it checks the soft cache
+ * for the cached bitmap. If neither of the caches contain the bitmap, this
+ * returns null.
+ */
+ private Bitmap getBitmapFromCache(String key){
+ synchronized(mHardBitmapCache) {
+ Bitmap bitmap = mHardBitmapCache.get(key);
+ if(bitmap != null){
+ // Put bitmap on top of cache so it's purged last.
+ mHardBitmapCache.remove(key);
+ mHardBitmapCache.put(key, bitmap);
+ return bitmap;
+ }
+ }
+
+ SoftReference<Bitmap> bitmapRef = mSoftBitmapCache.get(key);
+ if(bitmapRef != null){
+ Bitmap bitmap = bitmapRef.get();
+ if(bitmap != null){
+ return bitmap;
+ } else {
+ // Must have been collected by the Garbage Collector
+ // so we remove the bucket from the cache.
+ mSoftBitmapCache.remove(key);
+ }
+ }
+
+ // Could not locate the bitmap in any of the caches, so we return null.
+ return null;
+ }
+
+ /**
+ * @param parentFile The parentFile, so we can obtain the full path of the bitmap
+ * @param fileName The name of the file, also the text in the list item.
+ * @return The resized and resampled bitmap, if can not be decoded it returns null.
+ */
+ private Bitmap decodeFile(String parentFile, String fileName) {
+ if(!cancel){
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+
+ options.inJustDecodeBounds = true;
+ options.outWidth = 0;
+ options.outHeight = 0;
+ options.inSampleSize = 1;
+
+ String filePath = FileUtils.getFile(parentFile, fileName).getPath();
+
+ BitmapFactory.decodeFile(filePath, options);
+
+ if(options.outWidth > 0 && options.outHeight > 0){
+ if (!cancel) {
+ // Now see how much we need to scale it down.
+ int widthFactor = (options.outWidth + thumbnailWidth - 1)
+ / thumbnailWidth;
+ int heightFactor = (options.outHeight + thumbnailHeight - 1)
+ / thumbnailHeight;
+ widthFactor = Math.max(widthFactor, heightFactor);
+ widthFactor = Math.max(widthFactor, 1);
+ // Now turn it into a power of two.
+ if (widthFactor > 1) {
+ if ((widthFactor & (widthFactor - 1)) != 0) {
+ while ((widthFactor & (widthFactor - 1)) != 0) {
+ widthFactor &= widthFactor - 1;
+ }
+
+ widthFactor <<= 1;
+ }
+ }
+ options.inSampleSize = widthFactor;
+ options.inJustDecodeBounds = false;
+ Bitmap bitmap = ImageUtils.resizeBitmap(
+ BitmapFactory.decodeFile(filePath, options),
+ 72, 72);
+ if (bitmap != null) {
+ return bitmap;
+ }
+ }
+ } else {
+ // Must not be a bitmap, so we add it to the blacklist.
+ if(!mBlacklist.contains(fileName)){
+ mBlacklist.add(fileName);
+ }
+ }
+ } catch(Exception e) { }
+ }
+ return null;
+ }
+
+ /**
+ * Holder object for thumbnail information.
+ */
+ private class Thumbnail {
+ public String parentFile;
+ public ImageView imageView;
+ public IconifiedText text;
+
+ public Thumbnail(String parentFile, ImageView imageView, IconifiedText text) {
+ this.parentFile = parentFile;
+ this.imageView = imageView;
+ this.text = text;
+ }
+ }
+
+ /**
+ * Decodes the bitmap and sends a ThumbnailUpdater on the UI Thread
+ * to update the listitem and iconified text.
+ *
+ * @see ThumbnailUpdater
+ */
+ private class ThumbnailRunner implements Runnable {
+ Thumbnail thumb;
+ ThumbnailRunner(Thumbnail thumb){
+ this.thumb = thumb;
+ }
+
+ @Override
+ public void run() {
+ if(!cancel){
+ Bitmap bitmap = decodeFile(thumb.parentFile, thumb.text.getText());
+ if(bitmap != null && !cancel){
+ // Bitmap was successfully decoded so we place it in the hard cache.
+ mHardBitmapCache.put(thumb.text.getText(), bitmap);
+ Activity activity = ((Activity) mContext);
+ activity.runOnUiThread(new ThumbnailUpdater(bitmap, thumb));
+ thumb = null;
+ }
+ }
+ }
+ }
+
+ /**
+ * When run on the UI Thread, this updates the
+ * thumbnail in the corresponding iconifiedtext and imageview.
+ */
+ private class ThumbnailUpdater implements Runnable {
+ private Bitmap bitmap;
+ private Thumbnail thumb;
+
+ public ThumbnailUpdater(Bitmap bitmap, Thumbnail thumb) {
+ this.bitmap = bitmap;
+ this.thumb = thumb;
+ }
+
+ @Override
+ public void run() {
+ if(bitmap != null && mContext != null && !cancel){
+ thumb.imageView.setImageBitmap(bitmap);
+ thumb.text.setIcon(bitmap);
+ }
+ }
+ }
+}