package in.srain.cube.image.impl; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Bitmap.CompressFormat; import android.util.Log; import in.srain.cube.concurrent.SimpleExecutor; import in.srain.cube.concurrent.SimpleTask; import in.srain.cube.file.DiskLruCache; import in.srain.cube.file.DiskLruCache.Editor; import in.srain.cube.file.FileUtil; import in.srain.cube.image.iface.ImageFileProvider; import in.srain.cube.util.Debug; import java.io.*; /** * This class handles disk and memory caching of bitmaps. * <p/> * Most of the code is taken from the Android best practice of displaying Bitmaps <a href="http://developer.android.com/training/displaying-bitmaps/index.html">Displaying Bitmaps Efficiently</a>. * * @author http://www.liaohuqiu.net */ public class LruImageFileProvider implements ImageFileProvider { protected static final boolean DEBUG = Debug.DEBUG_IMAGE; protected static final String TAG = "image_provider"; private static final String DEFAULT_CACHE_DIR = "cube-image"; private static final int DEFAULT_CACHE_SIZE = 1024 * 1024 * 10; private static LruImageFileProvider sDefault; // Compression settings when writing images to disk cache private static final CompressFormat DEFAULT_COMPRESS_FORMAT = CompressFormat.JPEG; private static final int DEFAULT_COMPRESS_QUALITY = 70; private static final int DISK_CACHE_INDEX = 0; private DiskLruCache mDiskLruCache; private final Object mDiskCacheLock = new Object(); private boolean mDiskCacheStarting = true; private boolean mDiskCacheReady = false; private File mDiskCacheDir; private long mDiskCacheSize; private long mLastFlushTime = 0; @Override public FileInputStream getInputStream(String fileCacheKey) { return read(fileCacheKey); } @Override public FileInputStream downloadAndGetInputStream(String fileCacheKey, String url) { try { Editor editor = open(fileCacheKey); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); SimpleDownloader.downloadUrlToStream(url, outputStream); editor.commit(); } } catch (IOException e) { e.printStackTrace(); } return read(fileCacheKey); } protected enum FileCacheTaskType { init_cache, close_cache, flush_cache } public LruImageFileProvider(long size, File path) { mDiskCacheSize = size; mDiskCacheDir = path; } public static LruImageFileProvider getDefault(Context context) { if (null == sDefault) { FileUtil.CacheDirInfo cacheDirInfo = FileUtil.getDiskCacheDir(context, DEFAULT_CACHE_DIR, DEFAULT_CACHE_SIZE); sDefault = new LruImageFileProvider(cacheDirInfo.realSize, cacheDirInfo.path); sDefault.initDiskCacheAsync(); } return sDefault; } /** * Initializes the disk cache. Note that this includes disk access so this should not be executed on the main/UI thread. By default an ImageProvider does not initialize the disk cache when it is created, instead you should call initDiskCache() to initialize it on a background thread. */ public void initDiskCache() { if (DEBUG) { Log.d(TAG, "initDiskCache " + this); } // Set up disk cache synchronized (mDiskCacheLock) { if (mDiskLruCache == null || mDiskLruCache.isClosed()) { if (mDiskCacheDir != null) { if (!mDiskCacheDir.exists()) { mDiskCacheDir.mkdirs(); } if (FileUtil.getUsableSpace(mDiskCacheDir) > mDiskCacheSize) { try { mDiskLruCache = DiskLruCache.open(mDiskCacheDir, 1, 1, mDiskCacheSize); if (DEBUG) { Log.d(TAG, "Disk cache initialized " + this); } } catch (final IOException e) { Log.e(TAG, "initDiskCache - " + e); } } else { Log.e(TAG, String.format("no enough space for initDiskCache %s %s", FileUtil.getUsableSpace(mDiskCacheDir), mDiskCacheSize)); } } } mDiskCacheStarting = false; mDiskCacheReady = true; mDiskCacheLock.notifyAll(); } } /** * Adds a bitmap to both memory and disk cache * * @param key Unique identifier for the bitmap to store * @param bitmap The bitmap to store */ public void write(String key, Bitmap bitmap) { if (key == null || bitmap == null) { return; } synchronized (mDiskCacheLock) { // Add to disk cache if (mDiskLruCache != null) { OutputStream out = null; try { DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key); if (snapshot == null) { final DiskLruCache.Editor editor = mDiskLruCache.edit(key); if (editor != null) { out = editor.newOutputStream(DISK_CACHE_INDEX); bitmap.compress(DEFAULT_COMPRESS_FORMAT, DEFAULT_COMPRESS_QUALITY, out); editor.commit(); out.close(); } } } catch (final IOException e) { Log.e(TAG, "addBitmapToCache - " + e); } catch (Exception e) { Log.e(TAG, "addBitmapToCache - " + e); } finally { try { if (out != null) { out.close(); } } catch (IOException e) { } } } } } private FileInputStream read(String fileCacheKey) { if (!mDiskCacheReady) { initDiskCache(); } synchronized (mDiskCacheLock) { while (mDiskCacheStarting) { try { if (DEBUG) { Log.d(TAG, "read wait " + this); } mDiskCacheLock.wait(); } catch (InterruptedException e) { } } if (mDiskLruCache != null) { InputStream inputStream = null; DiskLruCache.Snapshot snapshot = null; try { snapshot = mDiskLruCache.get(fileCacheKey); } catch (final IOException e) { Log.e(TAG, "getBitmapFromDiskCache - " + e); } if (snapshot == null) { return null; } else { inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); return (FileInputStream) inputStream; } } return null; } } public Editor open(String key) throws IOException { if (null != mDiskLruCache) { return mDiskLruCache.edit(key); } else { Log.e(TAG, "mDiskLruCache is null"); return null; } } /** * Clears both the memory and disk cache associated with this ImageProvider object. Note that this includes disk access so this should not be executed on the main/UI thread. */ public void clearCache() { synchronized (mDiskCacheLock) { mDiskCacheStarting = true; mDiskCacheReady = false; if (mDiskLruCache != null && !mDiskLruCache.isClosed()) { try { mDiskLruCache.delete(); if (DEBUG) { Log.d(TAG, "Disk cache cleared"); } } catch (IOException e) { e.printStackTrace(); Log.e(TAG, "clearCache - " + e); } mDiskLruCache = null; initDiskCache(); } } } /** * Flushes the disk cache associated with this ImageProvider object. Note that this includes disk access so this should not be executed on the main/UI thread. */ public void flushDiskCache() { synchronized (mDiskCacheLock) { long now = System.currentTimeMillis(); if (now - 1000 < mLastFlushTime) { return; } mLastFlushTime = now; if (mDiskLruCache != null) { try { mDiskLruCache.flush(); if (DEBUG) { Log.d(TAG, "Disk cache flushed"); } } catch (IOException e) { Log.e(TAG, "flush - " + e); } } } } /** * Closes the disk cache associated with this ImageProvider object. Note that this includes disk access so this should not be executed on the main/UI thread. */ public void closeDiskCache() { synchronized (mDiskCacheLock) { if (mDiskLruCache != null) { try { if (!mDiskLruCache.isClosed()) { mDiskLruCache.close(); mDiskLruCache = null; if (DEBUG) { Log.d(TAG, "Disk cache closed"); } } } catch (IOException e) { Log.e(TAG, "close - " + e); } } } } /** * A helper class to encapsulate the operate into a Work which will be executed by the Worker. * * @author http://www.liaohuqiu.net */ private class FileCacheTask extends SimpleTask { private FileCacheTask(FileCacheTaskType taskType) { mTaskType = taskType; } private FileCacheTaskType mTaskType; @Override public void doInBackground() { switch (mTaskType) { case init_cache: initDiskCache(); break; case close_cache: closeDiskCache(); break; case flush_cache: flushDiskCache(); break; default: break; } } @Override public void onFinish() { } void excute() { SimpleExecutor.getInstance().execute(this); } } /** * initiate the disk cache */ public void initDiskCacheAsync() { if (DEBUG) { Log.d(TAG, "initDiskCacheAsync " + this); } new FileCacheTask(FileCacheTaskType.init_cache).excute(); } /** * close the disk cache */ public void closeDiskCacheAsync() { if (DEBUG) { Log.d(TAG, "closeDiskCacheAsync"); } new FileCacheTask(FileCacheTaskType.close_cache).excute(); } /** * flush the data to disk cache */ @Override public void flushDiskCacheAsync() { if (DEBUG) { Log.d(TAG, "flushDishCacheAsync"); } new FileCacheTask(FileCacheTaskType.flush_cache).excute(); } @Override public String getCachePath() { return mDiskCacheDir.getPath(); } @Override public long getUsedSpace() { if (null == mDiskLruCache) { return 0; } return mDiskLruCache.size(); } @Override public long getMaxSize() { return mDiskCacheSize; } @Override public boolean has(String key) { if (mDiskLruCache != null) { return read(key) != null; } return false; } }