package in.srain.cube.cache;
import android.content.Context;
import android.support.v4.util.LruCache;
import android.text.TextUtils;
import in.srain.cube.concurrent.SimpleExecutor;
import in.srain.cube.concurrent.SimpleTask;
import in.srain.cube.request.JsonData;
import in.srain.cube.util.CLog;
import in.srain.cube.util.CubeDebug;
import java.io.IOException;
/**
* @author http://www.liaohuqiu.net
*/
public class CacheManager {
private static final boolean DEBUG = CubeDebug.DEBUG_CACHE;
private static final String LOG_TAG = "cube-cache-manager";
private static final byte AFTER_READ_FROM_FILE = 0x01;
private static final byte AFTER_READ_FROM_ASSERT = 0x02;
private static final byte AFTER_CONVERT = 0x04;
private static final byte DO_READ_FROM_FILE = 0x01;
private static final byte DO_READ_FROM_ASSERT = 0x02;
private static final byte DO_CONVERT = 0x04;
private static final byte CONVERT_FOR_MEMORY = 0x03;
private static final byte CONVERT_FOR_FILE = 0x01;
private static final byte CONVERT_FOR_ASSERT = 0x02;
private static final byte CONVERT_FOR_CREATE = 0x04;
private LruCache<String, CacheMetaData> mMemoryCache;
private DiskCacheProvider mFileCache;
private Context mContext;
public CacheManager(Context content, String cacheDir, int memoryCacheSizeInKB, int fileCacheSizeInKB) {
mContext = content;
if (TextUtils.isEmpty(cacheDir)) {
throw new IllegalArgumentException("cacheDir can not be empty");
}
if (memoryCacheSizeInKB <= 0) {
throw new IllegalArgumentException("memoryCacheSizeInKB <= 0");
}
if (fileCacheSizeInKB <= 0) {
throw new IllegalArgumentException("fileCacheSizeInKB <= 0");
}
mMemoryCache = new LruCache<String, CacheMetaData>(memoryCacheSizeInKB * 1024) {
@Override
protected int sizeOf(String key, CacheMetaData value) {
return (value.getSize() + key.getBytes().length);
}
};
DiskFileUtils.CacheDirInfo cacheDirInfo = DiskFileUtils.getDiskCacheDir(content, cacheDir, fileCacheSizeInKB, null);
mFileCache = DiskCacheProvider.createLru(content, cacheDirInfo.path, cacheDirInfo.realSize);
if (DEBUG) {
CLog.d(LOG_TAG,
"CacheManger: cache dir: %s => %s, size: %s => %s",
cacheDir, cacheDirInfo.path, cacheDirInfo.requireSize, cacheDirInfo.realSize);
}
}
public <T> void requestCache(ICacheAble<T> cacheAble) {
InnerCacheTask<T> task = new InnerCacheTask<T>(cacheAble);
task.beginQuery();
}
public <T> void continueAfterCreateData(ICacheAble<T> cacheAble, final String data) {
setCacheData(cacheAble.getCacheKey(), data);
InnerCacheTask<T> task = new InnerCacheTask<T>(cacheAble);
task.beginConvertDataAsync(CONVERT_FOR_CREATE);
}
public void setCacheData(final String cacheKey, final String data) {
if (TextUtils.isEmpty(cacheKey) || TextUtils.isEmpty(data)) {
return;
}
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, setCacheData", cacheKey);
}
SimpleExecutor.getInstance().execute(
new Runnable() {
@Override
public void run() {
CacheMetaData cacheMetaData = CacheMetaData.createForNow(data);
putDataToMemoryCache(cacheKey, cacheMetaData);
mFileCache.write(cacheKey, cacheMetaData.getCacheData());
mFileCache.flushDiskCacheAsyncWithDelay(1000);
}
}
);
}
private void putDataToMemoryCache(String key, CacheMetaData data) {
if (TextUtils.isEmpty(key)) {
return;
}
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, set cache to runtime cache list", key);
}
mMemoryCache.put(key, data);
}
/**
* delete cache by key
*
* @param key
*/
public void invalidateCache(String key) {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, invalidateCache", key);
}
try {
mFileCache.getDiskCache().delete(key);
} catch (IOException e) {
e.printStackTrace();
}
mMemoryCache.remove(key);
}
/**
* clear the memory cache
*/
public void clearMemoryCache() {
if (mMemoryCache != null) {
mMemoryCache.evictAll();
}
}
/**
* get the spaced has been used
*
* @return
*/
public int getMemoryCacheUsedSpace() {
return mMemoryCache.size();
}
/**
* get the spaced max space in config
*
* @return
*/
public int getMemoryCacheMaxSpace() {
return mMemoryCache.maxSize();
}
/**
* clear the disk cache
*/
public void clearDiskCache() {
if (null != mFileCache) {
try {
mFileCache.getDiskCache().clear();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* return the file cache path
*
* @return
*/
public String getFileCachePath() {
if (null != mFileCache) {
return mFileCache.getDiskCache().getDirectory().getAbsolutePath();
}
return null;
}
/**
* get the used space in file cache
*
* @return
*/
public long getFileCacheUsedSpace() {
return null != mFileCache ? mFileCache.getDiskCache().getSize() : 0;
}
/**
* get the max space for file cache
*
* @return
*/
public long getFileCacheMaxSpace() {
if (null != mFileCache) {
return mFileCache.getDiskCache().getCapacity();
}
return 0;
}
/**
* Request cache synchronously.
* If there is not cache data available, return null,
* and {@link in.srain.cube.cache.ICacheAble#onNoCacheData} will not no be called.
*
* @param cacheAble
* @param <T>
* @return if not cache data available, return null, {@link in.srain.cube.cache.ICacheAble#onNoCacheData} will not no be called.
*/
@SuppressWarnings({"unused"})
public <T> T requestCacheSync(ICacheAble<T> cacheAble) {
if (cacheAble.cacheIsDisabled()) {
return null;
}
// try to find in runtime cache
String cacheKey = cacheAble.getCacheKey();
CacheMetaData mRawData = mMemoryCache.get(cacheKey);
if (mRawData != null) {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, exist in list", cacheKey);
}
}
// try read from cache data
if (mRawData == null) {
boolean hasFileCache = mFileCache.getDiskCache().has(cacheKey);
if (hasFileCache) {
String cacheContent = mFileCache.read(cacheKey);
JsonData jsonData = JsonData.create(cacheContent);
mRawData = CacheMetaData.createFromJson(jsonData);
}
}
// try to read from assets cache file
if (mRawData == null) {
String assertInitDataPath = cacheAble.getAssertInitDataPath();
if (assertInitDataPath != null && assertInitDataPath.length() > 0) {
String cacheContent = DiskFileUtils.readAssert(mContext, assertInitDataPath);
if (!TextUtils.isEmpty(cacheContent)) {
mRawData = CacheMetaData.createInvalidated(cacheContent);
putDataToMemoryCache(cacheKey, mRawData);
}
}
}
if (mRawData != null) {
boolean outOfDate = mRawData.isOutOfDateFor(cacheAble);
if (outOfDate && !cacheAble.useCacheAnyway()) {
return null;
}
JsonData data = JsonData.create(mRawData.getData());
T ret = cacheAble.processRawDataFromCache(data);
cacheAble.onCacheData(CacheResultType.FROM_INIT_FILE, ret, outOfDate);
return ret;
}
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, cache file not exist", cacheKey);
}
return null;
}
public DiskCacheProvider getDiskCacheProvider() {
return mFileCache;
}
private class InnerCacheTask<T1> extends SimpleTask {
private ICacheAble<T1> mCacheAble;
private CacheMetaData mRawData;
private T1 mResult;
private byte mWorkType = 0;
private byte mConvertFor = 0;
private byte mCurrentStatus = 0;
public InnerCacheTask(ICacheAble<T1> cacheAble) {
mCacheAble = cacheAble;
}
void beginQuery() {
String cacheKey = mCacheAble.getCacheKey();
if (mCacheAble.cacheIsDisabled()) {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, Cache is disabled, query from server", cacheKey);
}
mCacheAble.onNoCacheData(CacheManager.this);
return;
}
// try to find in runtime cache
mRawData = mMemoryCache.get(cacheKey);
if (mRawData != null) {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, exist in list", cacheKey);
}
beginConvertDataAsync(CONVERT_FOR_MEMORY);
return;
}
// try read from cache data
boolean hasFileCache = mFileCache.getDiskCache().has(cacheKey);
if (hasFileCache) {
beginQueryFromCacheFileAsync();
return;
}
// try to read from assert cache file
String assertInitDataPath = mCacheAble.getAssertInitDataPath();
if (assertInitDataPath != null && assertInitDataPath.length() > 0) {
beginQueryFromAssertCacheFileAsync();
return;
}
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, cache file not exist", mCacheAble.getCacheKey());
}
mCacheAble.onNoCacheData(CacheManager.this);
}
@Override
public void doInBackground() {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, doInBackground: mWorkType: %s", mCacheAble.getCacheKey(), mWorkType);
}
switch (mWorkType) {
case DO_READ_FROM_FILE:
doQueryFromCacheFileInBackground();
setCurrentStatus(AFTER_READ_FROM_FILE);
break;
case DO_READ_FROM_ASSERT:
doQueryFromAssertCacheFileInBackground();
setCurrentStatus(AFTER_READ_FROM_ASSERT);
break;
case DO_CONVERT:
doConvertDataInBackground();
setCurrentStatus(AFTER_CONVERT);
break;
default:
break;
}
}
@Override
public void onFinish(boolean canceled) {
switch (mCurrentStatus) {
case AFTER_READ_FROM_FILE:
beginConvertDataAsync(CONVERT_FOR_FILE);
break;
case AFTER_READ_FROM_ASSERT:
beginConvertDataAsync(CONVERT_FOR_ASSERT);
break;
case AFTER_CONVERT:
done();
break;
default:
break;
}
}
private void beginQueryFromCacheFileAsync() {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, beginQueryFromCacheFileAsync", mCacheAble.getCacheKey());
}
mWorkType = DO_READ_FROM_FILE;
restart();
SimpleExecutor.getInstance().execute(this);
}
private void beginQueryFromAssertCacheFileAsync() {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, beginQueryFromAssertCacheFileAsync", mCacheAble.getCacheKey());
}
mWorkType = DO_READ_FROM_ASSERT;
restart();
SimpleExecutor.getInstance().execute(this);
}
private void beginConvertDataAsync(byte convertFor) {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, beginConvertDataAsync", mCacheAble.getCacheKey());
}
mConvertFor = convertFor;
mWorkType = DO_CONVERT;
restart();
SimpleExecutor.getInstance().execute(this);
}
private void doQueryFromCacheFileInBackground() {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, try read cache data from file", mCacheAble.getCacheKey());
}
String cacheContent = mFileCache.read(mCacheAble.getCacheKey());
JsonData jsonData = JsonData.create(cacheContent);
mRawData = CacheMetaData.createFromJson(jsonData);
}
private void doQueryFromAssertCacheFileInBackground() {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, try read cache data from assert file", mCacheAble.getCacheKey());
}
String cacheContent = DiskFileUtils.readAssert(mContext, mCacheAble.getAssertInitDataPath());
mRawData = CacheMetaData.createInvalidated(cacheContent);
putDataToMemoryCache(mCacheAble.getCacheKey(), mRawData);
}
private void doConvertDataInBackground() {
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, doConvertDataInBackground", mCacheAble.getCacheKey());
}
JsonData data = JsonData.create(mRawData.getData());
mResult = mCacheAble.processRawDataFromCache(data);
}
private void setCurrentStatus(byte status) {
mCurrentStatus = status;
if (DEBUG) {
CLog.d(LOG_TAG, "key: %s, setCurrentStatus: %s", mCacheAble.getCacheKey(), status);
}
}
private void done() {
boolean outOfDate = mRawData.isOutOfDateFor(mCacheAble);
if (mResult != null) {
switch (mConvertFor) {
case CONVERT_FOR_ASSERT:
mCacheAble.onCacheData(CacheResultType.FROM_INIT_FILE, mResult, outOfDate);
break;
case CONVERT_FOR_CREATE:
mCacheAble.onCacheData(CacheResultType.FROM_CREATED, mResult, outOfDate);
break;
case CONVERT_FOR_FILE:
mCacheAble.onCacheData(CacheResultType.FROM_INIT_FILE, mResult, outOfDate);
break;
case CONVERT_FOR_MEMORY:
mCacheAble.onCacheData(CacheResultType.FROM_CACHE_FILE, mResult, outOfDate);
break;
}
}
if (mResult == null || outOfDate) {
mCacheAble.onNoCacheData(CacheManager.this);
}
}
}
}