package org.openintents.filemanager; import java.io.File; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.openintents.filemanager.files.FileHolder; import org.openintents.filemanager.util.FileUtils; import org.openintents.filemanager.util.ImageUtils; import org.openintents.filemanager.util.MimeTypes; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources.NotFoundException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.preference.PreferenceManager; import android.util.Log; import android.util.TypedValue; import android.widget.ImageView; public class ThumbnailLoader { private static final String MIME_APK = "application/vnd.android.package-archive"; 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 final boolean mUseBestMatch; private boolean cancel; private Context mContext; //private static int thumbnailWidth = 96; //private static int thumbnailHeight = 129; private static int thumbnailWidth = 96; private static int thumbnailHeight = 96; private Runnable purger; private Handler purgeHandler; private PausableThreadPoolExecutor 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 = new PausableThreadPoolExecutor(POOL_SIZE); mBlacklist = new ArrayList<>(); mSoftBitmapCache = new ConcurrentHashMap<>(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<>(eldest.getValue())); return true; } else { return false; } } }; mUseBestMatch = PreferenceManager.getDefaultSharedPreferences(context).getBoolean(PreferenceActivity.PREFS_USEBESTMATCH, true); } public static void setThumbnailHeight(int height) { thumbnailHeight = height; thumbnailWidth = height * 4 / 3; } /** * @param holder The {@link File} container. * @param imageView The ImageView from the IconifiedTextView. */ public void loadImage(FileHolder holder, ImageView imageView) { if(!cancel && !mBlacklist.contains(holder.getName())){ // We reset the caches after every 30 or so seconds of inactivity for memory efficiency. resetPurgeTimer(); Bitmap bitmap = getBitmapFromCache(holder.getName()); if(bitmap != null){ // We're still in the UI thread so we just update the icons from here. imageView.setImageBitmap(bitmap); holder.setIcon(new BitmapDrawable(bitmap)); } else { // Give a drawable based on mimetype. Generic file drawable for undefined types. if(holder.getFile().isFile()) holder.setIcon(getScaledDrawableForMimetype(holder, mContext)); if (!cancel) { // Submit the file for decoding. Thumbnail thumbnail = new Thumbnail(imageView, holder); ThumbnailRunner thumbnailRunner = new ThumbnailRunner(thumbnail); mExecutor.submit(thumbnailRunner); } } } } /** * 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; } /** * The file to decode. * @return The resized and resampled bitmap, if can not be decoded it returns null. */ private Bitmap decodeFile(File file) { if(!cancel){ try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; options.outWidth = 0; options.outHeight = 0; options.inSampleSize = 1; String filePath = file.getAbsolutePath(); 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 && (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(file.getName())){ mBlacklist.add(filePath); } } } catch(Exception e) { } } return null; } public void startProcessingLoaderQueue() { mExecutor.resume(); } public void stopProcessingLoaderQueue() { mExecutor.pause(); } /** * Holder object for thumbnail information. */ private class Thumbnail { public ImageView imageView; public FileHolder holder; public Thumbnail(ImageView imageView, FileHolder text) { this.imageView = imageView; this.holder = 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.holder.getFile()); Activity activity = (Activity) mContext; if(!cancel){ if(bitmap != null){ // Bitmap was successfully decoded so we place it in the hard cache. mHardBitmapCache.put(thumb.holder.getName(), bitmap); activity.runOnUiThread(new ThumbnailUpdater(bitmap, thumb)); } else { activity.runOnUiThread(new Runnable() { @Override public void run() { thumb.imageView.setImageDrawable(thumb.holder.getIcon()); 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.holder.setIcon(new BitmapDrawable(bitmap)); } thumb = null; } } private Drawable getScaledDrawableForMimetype(FileHolder holder, Context context){ Drawable d = getDrawableForMimetype(holder, context); if (d == null) { return new BitmapDrawable(context.getResources(), BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_launcher_file)); } else { int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48, context.getResources().getDisplayMetrics()); // Resizing image. return ImageUtils.resizeDrawable(d, size, size); } } /** * Return the Drawable that is associated with a specific mime type for the VIEW action. */ private Drawable getDrawableForMimetype(FileHolder holder, Context context) { if (holder.getMimeType() == null) { return null; } PackageManager pm = context.getPackageManager(); // Returns the icon packaged in files with the .apk MIME type. if (holder.getMimeType().equals(MIME_APK)) { String path = holder.getFile().getPath(); PackageInfo pInfo = pm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES); if (pInfo != null) { ApplicationInfo aInfo = pInfo.applicationInfo; // Bug in SDK versions >= 8. See here: // http://code.google.com/p/android/issues/detail?id=9151 if (Build.VERSION.SDK_INT >= 8) { aInfo.sourceDir = path; aInfo.publicSourceDir = path; } return aInfo.loadIcon(pm); } } int iconResource = MimeTypes.getInstance().getIcon(holder.getMimeType()); Drawable ret = null; if (iconResource > 0) { try { ret = pm.getResourcesForApplication(context.getPackageName()) .getDrawable(iconResource); } catch (NotFoundException|NameNotFoundException e) { } } if (ret != null) { return ret; } if ("*/*".equals(holder.getMimeType())){ return null; } Uri data = FileUtils.getUri(holder.getFile()); Intent intent = new Intent(Intent.ACTION_VIEW); // intent.setType(mimetype); // Let's probe the intent exactly in the same way as the VIEW action // is performed in FileManagerActivity.openFile(..) intent.setDataAndType(data, holder.getMimeType()); final List<ResolveInfo> lri = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); if (lri != null && !lri.isEmpty()) { // Log.i(TAG, "lri.size()" + lri.size()); // Actually first element should be "best match", // but it seems that more recently installed applications // could be even better match. int index = mUseBestMatch ? 0: lri.size() - 1; final ResolveInfo ri = lri.get(index); return ri.loadIcon(pm); } return null; } }