811a5a4a |
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 | } |