1 package org.openintents.filemanager;
3 import java.lang.ref.SoftReference;
4 import java.lang.ref.WeakReference;
5 import java.util.ArrayList;
6 import java.util.LinkedHashMap;
7 import java.util.concurrent.ConcurrentHashMap;
8 import java.util.concurrent.ExecutorService;
9 import java.util.concurrent.Executors;
11 import org.openintents.filemanager.util.FileUtils;
12 import org.openintents.filemanager.util.ImageUtils;
14 import android.app.Activity;
15 import android.content.Context;
16 import android.graphics.Bitmap;
17 import android.graphics.BitmapFactory;
18 import android.graphics.Canvas;
19 import android.graphics.Matrix;
20 import android.graphics.drawable.BitmapDrawable;
21 import android.graphics.drawable.Drawable;
22 import android.os.Handler;
23 import android.util.Log;
24 import android.widget.ImageView;
26 public class ThumbnailLoader {
28 private static final String TAG = "OIFM_ThumbnailLoader";
30 // Both hard and soft caches are purged after 40 seconds idling.
31 private static final int DELAY_BEFORE_PURGE = 40000;
32 private static final int MAX_CACHE_CAPACITY = 40;
34 // Maximum number of threads in the executor pool.
35 // TODO: Tune POOL_SIZE for maximum performance gain
36 private static final int POOL_SIZE = 5;
38 private boolean cancel;
39 private Context mContext;
41 //private static int thumbnailWidth = 96;
42 //private static int thumbnailHeight = 129;
43 private static int thumbnailWidth = 32;
44 private static int thumbnailHeight = 32;
46 private Runnable purger;
47 private Handler purgeHandler;
48 private ExecutorService mExecutor;
50 // Soft bitmap cache for thumbnails removed from the hard cache.
51 // This gets cleared by the Garbage Collector everytime we get low on memory.
52 private ConcurrentHashMap<String, SoftReference<Bitmap>> mSoftBitmapCache;
53 private LinkedHashMap<String, Bitmap> mHardBitmapCache;
54 private ArrayList<String> mBlacklist;
57 * Used for loading and decoding thumbnails from files.
60 * @param context Current application context.
62 public ThumbnailLoader(Context context) {
65 purger = new Runnable(){
68 Log.d(TAG, "Purge Timer hit; Clearing Caches.");
73 purgeHandler = new Handler();
74 mExecutor = Executors.newFixedThreadPool(POOL_SIZE);
76 mBlacklist = new ArrayList<String>();
77 mSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(MAX_CACHE_CAPACITY / 2);
78 mHardBitmapCache = new LinkedHashMap<String, Bitmap>(MAX_CACHE_CAPACITY / 2, 0.75f, true){
81 private static final long serialVersionUID = 1347795807259717646L;
84 protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest){
85 // Moves the last used item in the hard cache to the soft cache.
86 if(size() > MAX_CACHE_CAPACITY){
87 mSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
96 public static void setThumbnailHeight(int height) {
97 thumbnailHeight = height;
98 thumbnailWidth = height * 4 / 3;
103 * @param parentFile The current directory.
104 * @param text The IconifiedText container.
105 * @param imageView The ImageView from the IconifiedTextView.
107 public void loadImage(String parentFile, IconifiedText text, ImageView imageView) {
108 if(!cancel && !mBlacklist.contains(text.getText())){
109 // We reset the caches after every 30 or so seconds of inactivity for memory efficiency.
112 Bitmap bitmap = getBitmapFromCache(text.getText());
114 // We're still in the UI thread so we just update the icons from here.
115 imageView.setImageBitmap(bitmap);
116 text.setIcon(bitmap);
119 // Submit the file for decoding.
120 Thumbnail thumbnail = new Thumbnail(parentFile, imageView, text);
121 WeakReference<ThumbnailRunner> runner = new WeakReference<ThumbnailRunner>(new ThumbnailRunner(thumbnail));
122 mExecutor.submit(runner.get());
128 * Cancels any downloads, shuts down the executor pool,
129 * and then purges the caches.
131 public void cancel(){
134 // We could also terminate it immediately,
135 // but that may lead to synchronization issues.
136 if(!mExecutor.isShutdown()){
137 mExecutor.shutdown();
147 * Stops the cache purger from running until it is reset again.
149 public void stopPurgeTimer(){
150 purgeHandler.removeCallbacks(purger);
154 * Purges the cache every (DELAY_BEFORE_PURGE) milliseconds.
155 * @see DELAY_BEFORE_PURGE
157 private void resetPurgeTimer() {
158 purgeHandler.removeCallbacks(purger);
159 purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE);
162 private void clearCaches(){
163 mSoftBitmapCache.clear();
164 mHardBitmapCache.clear();
169 * @param key In this case the file name (used as the mapping id).
170 * @return bitmap The cached bitmap or null if it could not be located.
172 * As the name suggests, this method attemps to obtain a bitmap stored
173 * in one of the caches. First it checks the hard cache for the key.
174 * If a key is found, it moves the cached bitmap to the head of the cache
175 * so it gets moved to the soft cache last.
177 * If the hard cache doesn't contain the bitmap, it checks the soft cache
178 * for the cached bitmap. If neither of the caches contain the bitmap, this
181 private Bitmap getBitmapFromCache(String key){
182 synchronized(mHardBitmapCache) {
183 Bitmap bitmap = mHardBitmapCache.get(key);
185 // Put bitmap on top of cache so it's purged last.
186 mHardBitmapCache.remove(key);
187 mHardBitmapCache.put(key, bitmap);
192 SoftReference<Bitmap> bitmapRef = mSoftBitmapCache.get(key);
193 if(bitmapRef != null){
194 Bitmap bitmap = bitmapRef.get();
198 // Must have been collected by the Garbage Collector
199 // so we remove the bucket from the cache.
200 mSoftBitmapCache.remove(key);
204 // Could not locate the bitmap in any of the caches, so we return null.
209 * @param parentFile The parentFile, so we can obtain the full path of the bitmap
210 * @param fileName The name of the file, also the text in the list item.
211 * @return The resized and resampled bitmap, if can not be decoded it returns null.
213 private Bitmap decodeFile(String parentFile, String fileName) {
216 BitmapFactory.Options options = new BitmapFactory.Options();
218 options.inJustDecodeBounds = true;
219 options.outWidth = 0;
220 options.outHeight = 0;
221 options.inSampleSize = 1;
223 String filePath = FileUtils.getFile(parentFile, fileName).getPath();
225 BitmapFactory.decodeFile(filePath, options);
227 if(options.outWidth > 0 && options.outHeight > 0){
229 // Now see how much we need to scale it down.
230 int widthFactor = (options.outWidth + thumbnailWidth - 1)
232 int heightFactor = (options.outHeight + thumbnailHeight - 1)
234 widthFactor = Math.max(widthFactor, heightFactor);
235 widthFactor = Math.max(widthFactor, 1);
236 // Now turn it into a power of two.
237 if (widthFactor > 1) {
238 if ((widthFactor & (widthFactor - 1)) != 0) {
239 while ((widthFactor & (widthFactor - 1)) != 0) {
240 widthFactor &= widthFactor - 1;
246 options.inSampleSize = widthFactor;
247 options.inJustDecodeBounds = false;
248 Bitmap bitmap = ImageUtils.resizeBitmap(
249 BitmapFactory.decodeFile(filePath, options),
251 if (bitmap != null) {
256 // Must not be a bitmap, so we add it to the blacklist.
257 if(!mBlacklist.contains(fileName)){
258 mBlacklist.add(fileName);
261 } catch(Exception e) { }
267 * Holder object for thumbnail information.
269 private class Thumbnail {
270 public String parentFile;
271 public ImageView imageView;
272 public IconifiedText text;
274 public Thumbnail(String parentFile, ImageView imageView, IconifiedText text) {
275 this.parentFile = parentFile;
276 this.imageView = imageView;
282 * Decodes the bitmap and sends a ThumbnailUpdater on the UI Thread
283 * to update the listitem and iconified text.
285 * @see ThumbnailUpdater
287 private class ThumbnailRunner implements Runnable {
289 ThumbnailRunner(Thumbnail thumb){
296 Bitmap bitmap = decodeFile(thumb.parentFile, thumb.text.getText());
297 if(bitmap != null && !cancel){
298 // Bitmap was successfully decoded so we place it in the hard cache.
299 mHardBitmapCache.put(thumb.text.getText(), bitmap);
300 Activity activity = ((Activity) mContext);
301 activity.runOnUiThread(new ThumbnailUpdater(bitmap, thumb));
309 * When run on the UI Thread, this updates the
310 * thumbnail in the corresponding iconifiedtext and imageview.
312 private class ThumbnailUpdater implements Runnable {
313 private Bitmap bitmap;
314 private Thumbnail thumb;
316 public ThumbnailUpdater(Bitmap bitmap, Thumbnail thumb) {
317 this.bitmap = bitmap;
323 if(bitmap != null && mContext != null && !cancel){
324 thumb.imageView.setImageBitmap(bitmap);
325 thumb.text.setIcon(bitmap);