| 1 | package org.openintents.filemanager; |
| 2 | |
| 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; |
| 10 | |
| 11 | import org.openintents.filemanager.util.FileUtils; |
| 12 | import org.openintents.filemanager.util.ImageUtils; |
| 13 | |
| 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; |
| 25 | |
| 26 | public class ThumbnailLoader { |
| 27 | |
| 28 | private static final String TAG = "OIFM_ThumbnailLoader"; |
| 29 | |
| 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; |
| 33 | |
| 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; |
| 37 | |
| 38 | private boolean cancel; |
| 39 | private Context mContext; |
| 40 | |
| 41 | //private static int thumbnailWidth = 96; |
| 42 | //private static int thumbnailHeight = 129; |
| 43 | private static int thumbnailWidth = 32; |
| 44 | private static int thumbnailHeight = 32; |
| 45 | |
| 46 | private Runnable purger; |
| 47 | private Handler purgeHandler; |
| 48 | private ExecutorService mExecutor; |
| 49 | |
| 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; |
| 55 | |
| 56 | /** |
| 57 | * Used for loading and decoding thumbnails from files. |
| 58 | * |
| 59 | * @author PhilipHayes |
| 60 | * @param context Current application context. |
| 61 | */ |
| 62 | public ThumbnailLoader(Context context) { |
| 63 | mContext = context; |
| 64 | |
| 65 | purger = new Runnable(){ |
| 66 | @Override |
| 67 | public void run() { |
| 68 | Log.d(TAG, "Purge Timer hit; Clearing Caches."); |
| 69 | clearCaches(); |
| 70 | } |
| 71 | }; |
| 72 | |
| 73 | purgeHandler = new Handler(); |
| 74 | mExecutor = Executors.newFixedThreadPool(POOL_SIZE); |
| 75 | |
| 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){ |
| 79 | |
| 80 | /***/ |
| 81 | private static final long serialVersionUID = 1347795807259717646L; |
| 82 | |
| 83 | @Override |
| 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())); |
| 88 | return true; |
| 89 | } else { |
| 90 | return false; |
| 91 | } |
| 92 | } |
| 93 | }; |
| 94 | } |
| 95 | |
| 96 | public static void setThumbnailHeight(int height) { |
| 97 | thumbnailHeight = height; |
| 98 | thumbnailWidth = height * 4 / 3; |
| 99 | } |
| 100 | |
| 101 | /** |
| 102 | * |
| 103 | * @param parentFile The current directory. |
| 104 | * @param text The IconifiedText container. |
| 105 | * @param imageView The ImageView from the IconifiedTextView. |
| 106 | */ |
| 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. |
| 110 | resetPurgeTimer(); |
| 111 | |
| 112 | Bitmap bitmap = getBitmapFromCache(text.getText()); |
| 113 | if(bitmap != null){ |
| 114 | // We're still in the UI thread so we just update the icons from here. |
| 115 | imageView.setImageBitmap(bitmap); |
| 116 | text.setIcon(bitmap); |
| 117 | } else { |
| 118 | if (!cancel) { |
| 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()); |
| 123 | } |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | /** |
| 128 | * Cancels any downloads, shuts down the executor pool, |
| 129 | * and then purges the caches. |
| 130 | */ |
| 131 | public void cancel(){ |
| 132 | cancel = true; |
| 133 | |
| 134 | // We could also terminate it immediately, |
| 135 | // but that may lead to synchronization issues. |
| 136 | if(!mExecutor.isShutdown()){ |
| 137 | mExecutor.shutdown(); |
| 138 | } |
| 139 | |
| 140 | stopPurgeTimer(); |
| 141 | |
| 142 | mContext = null; |
| 143 | clearCaches(); |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Stops the cache purger from running until it is reset again. |
| 148 | */ |
| 149 | public void stopPurgeTimer(){ |
| 150 | purgeHandler.removeCallbacks(purger); |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * Purges the cache every (DELAY_BEFORE_PURGE) milliseconds. |
| 155 | * @see DELAY_BEFORE_PURGE |
| 156 | */ |
| 157 | private void resetPurgeTimer() { |
| 158 | purgeHandler.removeCallbacks(purger); |
| 159 | purgeHandler.postDelayed(purger, DELAY_BEFORE_PURGE); |
| 160 | } |
| 161 | |
| 162 | private void clearCaches(){ |
| 163 | mSoftBitmapCache.clear(); |
| 164 | mHardBitmapCache.clear(); |
| 165 | mBlacklist.clear(); |
| 166 | } |
| 167 | |
| 168 | /** |
| 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. |
| 171 | * |
| 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. |
| 176 | * |
| 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 |
| 179 | * returns null. |
| 180 | */ |
| 181 | private Bitmap getBitmapFromCache(String key){ |
| 182 | synchronized(mHardBitmapCache) { |
| 183 | Bitmap bitmap = mHardBitmapCache.get(key); |
| 184 | if(bitmap != null){ |
| 185 | // Put bitmap on top of cache so it's purged last. |
| 186 | mHardBitmapCache.remove(key); |
| 187 | mHardBitmapCache.put(key, bitmap); |
| 188 | return bitmap; |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | SoftReference<Bitmap> bitmapRef = mSoftBitmapCache.get(key); |
| 193 | if(bitmapRef != null){ |
| 194 | Bitmap bitmap = bitmapRef.get(); |
| 195 | if(bitmap != null){ |
| 196 | return bitmap; |
| 197 | } else { |
| 198 | // Must have been collected by the Garbage Collector |
| 199 | // so we remove the bucket from the cache. |
| 200 | mSoftBitmapCache.remove(key); |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | // Could not locate the bitmap in any of the caches, so we return null. |
| 205 | return null; |
| 206 | } |
| 207 | |
| 208 | /** |
| 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. |
| 212 | */ |
| 213 | private Bitmap decodeFile(String parentFile, String fileName) { |
| 214 | if(!cancel){ |
| 215 | try { |
| 216 | BitmapFactory.Options options = new BitmapFactory.Options(); |
| 217 | |
| 218 | options.inJustDecodeBounds = true; |
| 219 | options.outWidth = 0; |
| 220 | options.outHeight = 0; |
| 221 | options.inSampleSize = 1; |
| 222 | |
| 223 | String filePath = FileUtils.getFile(parentFile, fileName).getPath(); |
| 224 | |
| 225 | BitmapFactory.decodeFile(filePath, options); |
| 226 | |
| 227 | if(options.outWidth > 0 && options.outHeight > 0){ |
| 228 | if (!cancel) { |
| 229 | // Now see how much we need to scale it down. |
| 230 | int widthFactor = (options.outWidth + thumbnailWidth - 1) |
| 231 | / thumbnailWidth; |
| 232 | int heightFactor = (options.outHeight + thumbnailHeight - 1) |
| 233 | / thumbnailHeight; |
| 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; |
| 241 | } |
| 242 | |
| 243 | widthFactor <<= 1; |
| 244 | } |
| 245 | } |
| 246 | options.inSampleSize = widthFactor; |
| 247 | options.inJustDecodeBounds = false; |
| 248 | Bitmap bitmap = ImageUtils.resizeBitmap( |
| 249 | BitmapFactory.decodeFile(filePath, options), |
| 250 | 72, 72); |
| 251 | if (bitmap != null) { |
| 252 | return bitmap; |
| 253 | } |
| 254 | } |
| 255 | } else { |
| 256 | // Must not be a bitmap, so we add it to the blacklist. |
| 257 | if(!mBlacklist.contains(fileName)){ |
| 258 | mBlacklist.add(fileName); |
| 259 | } |
| 260 | } |
| 261 | } catch(Exception e) { } |
| 262 | } |
| 263 | return null; |
| 264 | } |
| 265 | |
| 266 | /** |
| 267 | * Holder object for thumbnail information. |
| 268 | */ |
| 269 | private class Thumbnail { |
| 270 | public String parentFile; |
| 271 | public ImageView imageView; |
| 272 | public IconifiedText text; |
| 273 | |
| 274 | public Thumbnail(String parentFile, ImageView imageView, IconifiedText text) { |
| 275 | this.parentFile = parentFile; |
| 276 | this.imageView = imageView; |
| 277 | this.text = text; |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Decodes the bitmap and sends a ThumbnailUpdater on the UI Thread |
| 283 | * to update the listitem and iconified text. |
| 284 | * |
| 285 | * @see ThumbnailUpdater |
| 286 | */ |
| 287 | private class ThumbnailRunner implements Runnable { |
| 288 | Thumbnail thumb; |
| 289 | ThumbnailRunner(Thumbnail thumb){ |
| 290 | this.thumb = thumb; |
| 291 | } |
| 292 | |
| 293 | @Override |
| 294 | public void run() { |
| 295 | if(!cancel){ |
| 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)); |
| 302 | thumb = null; |
| 303 | } |
| 304 | } |
| 305 | } |
| 306 | } |
| 307 | |
| 308 | /** |
| 309 | * When run on the UI Thread, this updates the |
| 310 | * thumbnail in the corresponding iconifiedtext and imageview. |
| 311 | */ |
| 312 | private class ThumbnailUpdater implements Runnable { |
| 313 | private Bitmap bitmap; |
| 314 | private Thumbnail thumb; |
| 315 | |
| 316 | public ThumbnailUpdater(Bitmap bitmap, Thumbnail thumb) { |
| 317 | this.bitmap = bitmap; |
| 318 | this.thumb = thumb; |
| 319 | } |
| 320 | |
| 321 | @Override |
| 322 | public void run() { |
| 323 | if(bitmap != null && mContext != null && !cancel){ |
| 324 | thumb.imageView.setImageBitmap(bitmap); |
| 325 | thumb.text.setIcon(bitmap); |
| 326 | } |
| 327 | } |
| 328 | } |
| 329 | } |