X-Git-Url: https://notaz.gp2x.de/cgi-bin/gitweb.cgi?p=android_pandora.git;a=blobdiff_plain;f=apps%2Foi-filemanager%2FFileManager%2Fsrc%2Forg%2Fopenintents%2Ffilemanager%2FThumbnailLoader.java;fp=apps%2Foi-filemanager%2FFileManager%2Fsrc%2Forg%2Fopenintents%2Ffilemanager%2FThumbnailLoader.java;h=b6d4d15d1cc14ce09b7431de5df906ff9b47d596;hp=0000000000000000000000000000000000000000;hb=811a5a4a3091f65fef340acafe62d6355b13c44f;hpb=4401ca4aa1b3938939c6c371dfda57aa0652696f diff --git a/apps/oi-filemanager/FileManager/src/org/openintents/filemanager/ThumbnailLoader.java b/apps/oi-filemanager/FileManager/src/org/openintents/filemanager/ThumbnailLoader.java new file mode 100644 index 0000000..b6d4d15 --- /dev/null +++ b/apps/oi-filemanager/FileManager/src/org/openintents/filemanager/ThumbnailLoader.java @@ -0,0 +1,329 @@ +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> mSoftBitmapCache; + private LinkedHashMap mHardBitmapCache; + private ArrayList 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(); + mSoftBitmapCache = new ConcurrentHashMap>(MAX_CACHE_CAPACITY / 2); + mHardBitmapCache = new LinkedHashMap(MAX_CACHE_CAPACITY / 2, 0.75f, true){ + + /***/ + private static final long serialVersionUID = 1347795807259717646L; + + @Override + protected boolean removeEldestEntry(LinkedHashMap.Entry 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(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 runner = new WeakReference(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 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); + } + } + } +}