toEvict = map.entrySet().iterator().next();
+ key = toEvict.getKey();
+ value = toEvict.getValue();
+ map.remove(key);
+ size -= safeSizeOf(key, value);
+ evictionCount++;
+ }
+
+ entryRemoved(true, key, value, null);
+ }
+ }
+
+ /**
+ * Removes the entry for {@code key} if it exists.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V remove(K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+
+ V previous;
+ synchronized (this) {
+ previous = map.remove(key);
+ if (previous != null) {
+ size -= safeSizeOf(key, previous);
+ }
+ }
+
+ if (previous != null) {
+ entryRemoved(false, key, previous, null);
+ }
+
+ return previous;
+ }
+
+ /**
+ * Called for entries that have been evicted or removed. This method is
+ * invoked when a value is evicted to make space, removed by a call to
+ * {@link #remove}, or replaced by a call to {@link #put}. The default
+ * implementation does nothing.
+ * The method is called without synchronization: other threads may
+ * access the cache while this method is executing.
+ *
+ * @param evicted true if the entry is being removed to make space, false
+ * if the removal was caused by a {@link #put} or {@link #remove}.
+ * @param newValue the new value for {@code key}, if it exists. If non-null,
+ * this removal was caused by a {@link #put}. Otherwise it was caused by
+ * an eviction or a {@link #remove}.
+ */
+ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {
+ }
+
+ /**
+ * Called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The
+ * default implementation returns null.
+ *
The method is called without synchronization: other threads may
+ * access the cache while this method is executing.
+ *
If a value for {@code key} exists in the cache when this method
+ * returns, the created value will be released with {@link #entryRemoved}
+ * and discarded. This can occur when multiple threads request the same key
+ * at the same time (causing multiple values to be created), or when one
+ * thread calls {@link #put} while another is creating a value for the same
+ * key.
+ */
+ protected V create(K key) {
+ return null;
+ }
+
+ private int safeSizeOf(K key, V value) {
+ int result = sizeOf(key, value);
+ if (result < 0) {
+ throw new IllegalStateException("Negative size: " + key + "=" + value);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the size of the entry for {@code key} and {@code value} in
+ * user-defined units. The default implementation returns 1 so that size
+ * is the number of entries and max size is the maximum number of entries.
+ *
An entry's size must not change while it is in the cache.
+ */
+ protected int sizeOf(K key, V value) {
+ return 1;
+ }
+
+ /**
+ * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+ */
+ public final void evictAll() {
+ trimToSize(-1); // -1 will evict 0-sized elements
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the number
+ * of entries in the cache. For all other caches, this returns the sum of
+ * the sizes of the entries in this cache.
+ */
+ public synchronized final int size() {
+ return size;
+ }
+
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the maximum
+ * number of entries in the cache. For all other caches, this returns the
+ * maximum sum of the sizes of the entries in this cache.
+ */
+ public synchronized final int maxSize() {
+ return maxSize;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned a value that was
+ * already present in the cache.
+ */
+ public synchronized final int hitCount() {
+ return hitCount;
+ }
+
+ /**
+ * Returns the number of times {@link #get} returned null or required a new
+ * value to be created.
+ */
+ public synchronized final int missCount() {
+ return missCount;
+ }
+
+ /**
+ * Returns the number of times {@link #create(Object)} returned a value.
+ */
+ public synchronized final int createCount() {
+ return createCount;
+ }
+
+ /**
+ * Returns the number of times {@link #put} was called.
+ */
+ public synchronized final int putCount() {
+ return putCount;
+ }
+
+ /**
+ * Returns the number of values that have been evicted.
+ */
+ public synchronized final int evictionCount() {
+ return evictionCount;
+ }
+
+ /**
+ * Returns a copy of the current contents of the cache, ordered from least
+ * recently accessed to most recently accessed.
+ */
+ public synchronized final Map snapshot() {
+ return new LinkedHashMap(map);
+ }
+
+ @Override
+ public synchronized final String toString() {
+ int accesses = hitCount + missCount;
+ int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
+ return String.format(Locale.getDefault(),
+ "LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
+ maxSize, hitCount, missCount, hitPercent);
+ }
+}
diff --git a/app/src/main/java/org/xutils/cache/LruDiskCache.java b/app/src/main/java/org/xutils/cache/LruDiskCache.java
new file mode 100644
index 0000000..8101a63
--- /dev/null
+++ b/app/src/main/java/org/xutils/cache/LruDiskCache.java
@@ -0,0 +1,391 @@
+package org.xutils.cache;
+
+
+import android.text.TextUtils;
+
+import org.xutils.DbManager;
+import org.xutils.common.task.PriorityExecutor;
+import org.xutils.common.util.FileUtil;
+import org.xutils.common.util.IOUtil;
+import org.xutils.common.util.LogUtil;
+import org.xutils.common.util.MD5;
+import org.xutils.common.util.ProcessLock;
+import org.xutils.config.DbConfigs;
+import org.xutils.db.sqlite.WhereBuilder;
+import org.xutils.ex.FileLockedException;
+import org.xutils.x;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Created by wyouflf on 15/7/23.
+ * 使用sqlite索引实现的LruDiskCache
+ */
+public final class LruDiskCache {
+
+ /**
+ * key: cacheDirName
+ */
+ private static final HashMap DISK_CACHE_MAP = new HashMap(5);
+
+ private static final int LIMIT_COUNT = 5000; // 限制最多5000条数据
+ private static final long LIMIT_SIZE = 1024L * 1024L * 100L; // 限制最多100M文件
+
+ private static final int LOCK_WAIT = 1000 * 3; // 3s
+ private static final String CACHE_DIR_NAME = "xUtils_cache";
+ private static final String TEMP_FILE_SUFFIX = ".tmp";
+
+ private boolean available = false;
+ private DbManager cacheDb;
+ private File cacheDir;
+ private long diskCacheSize = LIMIT_SIZE;
+ private final Executor trimExecutor = new PriorityExecutor(1, true);
+
+ private long lastTrimTime = 0L;
+ private static final long TRIM_TIME_SPAN = 1000;
+
+ public synchronized static LruDiskCache getDiskCache(String dirName) {
+ if (TextUtils.isEmpty(dirName)) dirName = CACHE_DIR_NAME;
+ LruDiskCache cache = DISK_CACHE_MAP.get(dirName);
+ if (cache == null) {
+ cache = new LruDiskCache(dirName);
+ DISK_CACHE_MAP.put(dirName, cache);
+ }
+ return cache;
+ }
+
+ private LruDiskCache(String dirName) {
+ try {
+ this.cacheDir = FileUtil.getCacheDir(dirName);
+ if (this.cacheDir != null && (this.cacheDir.exists() || this.cacheDir.mkdirs())) {
+ available = true;
+ }
+ this.cacheDb = x.getDb(DbConfigs.HTTP.getConfig());
+ } catch (Throwable ex) {
+ available = false;
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ deleteNoIndexFiles();
+ }
+
+ public LruDiskCache setMaxSize(long maxSize) {
+ if (maxSize > 0L) {
+ long diskFreeSize = FileUtil.getDiskAvailableSize();
+ if (diskFreeSize > maxSize) {
+ diskCacheSize = maxSize;
+ } else {
+ diskCacheSize = diskFreeSize;
+ }
+ }
+ return this;
+ }
+
+ public DiskCacheEntity get(String key) {
+ if (!available || TextUtils.isEmpty(key)) return null;
+
+ DiskCacheEntity result = null;
+ try {
+ result = this.cacheDb.selector(DiskCacheEntity.class)
+ .where("key", "=", key).findFirst();
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+
+ if (result != null) {
+
+ if (result.getExpires() < System.currentTimeMillis()) {
+ return null;
+ }
+
+ { // update hint & lastAccess...
+ final DiskCacheEntity finalResult = result;
+ trimExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ finalResult.setHits(finalResult.getHits() + 1);
+ finalResult.setLastAccess(System.currentTimeMillis());
+ try {
+ cacheDb.update(finalResult, "hits", "lastAccess");
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ }
+ });
+ }
+
+ }
+
+ return result;
+ }
+
+ public void put(DiskCacheEntity entity) {
+ if (!available
+ || entity == null
+ || TextUtils.isEmpty(entity.getTextContent())
+ || entity.getExpires() < System.currentTimeMillis()) {
+ return;
+ }
+
+ try {
+ cacheDb.replace(entity);
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+
+ trimSize();
+ }
+
+ public DiskCacheFile getDiskCacheFile(String key) throws InterruptedException {
+ if (!available || TextUtils.isEmpty(key)) {
+ return null;
+ }
+
+ DiskCacheFile result = null;
+ DiskCacheEntity entity = get(key);
+ if (entity != null && new File(entity.getPath()).exists()) {
+ ProcessLock processLock = ProcessLock.tryLock(entity.getPath(), false, LOCK_WAIT);
+ if (processLock != null && processLock.isValid()) {
+ result = new DiskCacheFile(entity.getPath(), entity, processLock);
+ if (!result.exists()) {
+ try {
+ cacheDb.delete(entity);
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ result = null;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public DiskCacheFile createDiskCacheFile(DiskCacheEntity entity) throws IOException {
+ if (!available || entity == null) {
+ return null;
+ }
+
+ DiskCacheFile result = null;
+
+ entity.setPath(new File(this.cacheDir, MD5.md5(entity.getKey())).getAbsolutePath());
+ String tempFilePath = entity.getPath() + TEMP_FILE_SUFFIX;
+ ProcessLock processLock = ProcessLock.tryLock(tempFilePath, true);
+ if (processLock != null && processLock.isValid()) {
+ result = new DiskCacheFile(tempFilePath, entity, processLock);
+ if (!result.getParentFile().exists()) {
+ result.mkdirs();
+ }
+ } else {
+ throw new FileLockedException(entity.getPath());
+ }
+
+ return result;
+ }
+
+ public void clearCacheFiles() {
+ IOUtil.deleteFileOrDir(cacheDir);
+ }
+
+ /**
+ * 添加缓存文件
+ *
+ * @param cacheFile
+ */
+ /*package*/ DiskCacheFile commitDiskCacheFile(DiskCacheFile cacheFile) throws IOException {
+ if (!available || cacheFile == null) {
+ return cacheFile;
+ }
+
+ DiskCacheFile result = null;
+ DiskCacheEntity cacheEntity = cacheFile.getCacheEntity();
+ if (cacheFile.getName().endsWith(TEMP_FILE_SUFFIX)) { // is temp file
+ ProcessLock processLock = null;
+ DiskCacheFile destFile = null;
+ try {
+ String destPath = cacheEntity.getPath();
+ processLock = ProcessLock.tryLock(destPath, true, LOCK_WAIT);
+ if (processLock != null && processLock.isValid()) { // lock
+ destFile = new DiskCacheFile(destPath, cacheEntity, processLock);
+ if (cacheFile.renameTo(destFile)) {
+ try {
+ result = destFile;
+ cacheDb.replace(cacheEntity);
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+
+ trimSize();
+ } else {
+ throw new IOException("rename:" + cacheFile.getAbsolutePath());
+ }
+ } else {
+ throw new FileLockedException(destPath);
+ }
+ } catch (InterruptedException ex) {
+ result = cacheFile;
+ LogUtil.e(ex.getMessage(), ex);
+ } finally {
+ if (result == null) {
+ result = cacheFile;
+ IOUtil.closeQuietly(destFile);
+ IOUtil.closeQuietly(processLock);
+ IOUtil.deleteFileOrDir(destFile);
+ } else {
+ IOUtil.closeQuietly(cacheFile);
+ IOUtil.deleteFileOrDir(cacheFile);
+ }
+ }
+ } else {
+ result = cacheFile;
+ }
+
+ return result;
+ }
+
+ private void trimSize() {
+ trimExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (!available) return;
+
+ long current = System.currentTimeMillis();
+ if (current - lastTrimTime < TRIM_TIME_SPAN) {
+ return;
+ } else {
+ lastTrimTime = current;
+ }
+
+ // trim expires
+ deleteExpiry();
+
+ // trim db
+ try {
+ int count = (int) cacheDb.selector(DiskCacheEntity.class).count();
+ if (count > LIMIT_COUNT + 10) {
+ List rmList = cacheDb.selector(DiskCacheEntity.class)
+ .orderBy("lastAccess").orderBy("hits")
+ .limit(count - LIMIT_COUNT).offset(0).findAll();
+ if (rmList != null && rmList.size() > 0) {
+ // delete cache files
+ for (DiskCacheEntity entity : rmList) {
+ try {
+ // delete db entity
+ cacheDb.delete(entity);
+ // delete cache files
+ String path = entity.getPath();
+ if (!TextUtils.isEmpty(path)) {
+ deleteFileWithLock(path);
+ deleteFileWithLock(path + TEMP_FILE_SUFFIX);
+ }
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ }
+
+ }
+ }
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+
+ // trim disk
+ try {
+ while (FileUtil.getFileOrDirSize(cacheDir) > diskCacheSize) {
+ List rmList = cacheDb.selector(DiskCacheEntity.class)
+ .orderBy("lastAccess").orderBy("hits").limit(10).offset(0).findAll();
+ if (rmList != null && rmList.size() > 0) {
+ // delete cache files
+ for (DiskCacheEntity entity : rmList) {
+ try {
+ // delete db entity
+ cacheDb.delete(entity);
+ // delete cache files
+ String path = entity.getPath();
+ if (!TextUtils.isEmpty(path)) {
+ deleteFileWithLock(path);
+ deleteFileWithLock(path + TEMP_FILE_SUFFIX);
+ }
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ }
+ }
+ }
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ }
+ });
+ }
+
+ private void deleteExpiry() {
+ if (!available) return;
+
+ try {
+ WhereBuilder whereBuilder = WhereBuilder.b("expires", "<", System.currentTimeMillis());
+ List rmList = cacheDb.selector(DiskCacheEntity.class).where(whereBuilder).findAll();
+ // delete db entities
+ cacheDb.delete(DiskCacheEntity.class, whereBuilder);
+ if (rmList != null && rmList.size() > 0) {
+ // delete cache files
+ for (DiskCacheEntity entity : rmList) {
+ String path = entity.getPath();
+ if (!TextUtils.isEmpty(path)) {
+ deleteFileWithLock(path);
+ }
+ }
+ }
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ }
+
+ /**
+ * 清理未被数据库索引的历史缓存文件
+ */
+ private void deleteNoIndexFiles() {
+ trimExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (!available) return;
+
+ try {
+ File[] fileList = cacheDir.listFiles();
+ if (fileList != null) {
+ for (File file : fileList) {
+ try {
+ long count = cacheDb.selector(DiskCacheEntity.class)
+ .where("path", "=", file.getAbsolutePath()).count();
+ if (count < 1) {
+ IOUtil.deleteFileOrDir(file);
+ }
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ }
+ }
+ } catch (Throwable ex) {
+ LogUtil.e(ex.getMessage(), ex);
+ }
+ }
+ });
+ }
+
+ private boolean deleteFileWithLock(String path) {
+ ProcessLock processLock = null;
+ try {
+ processLock = ProcessLock.tryLock(path, true);
+ if (processLock != null && processLock.isValid()) { // lock
+ File file = new File(path);
+ return IOUtil.deleteFileOrDir(file);
+ }
+ } finally {
+ IOUtil.closeQuietly(processLock);
+ }
+ return false;
+ }
+}
diff --git a/app/src/main/java/org/xutils/common/Callback.java b/app/src/main/java/org/xutils/common/Callback.java
new file mode 100644
index 0000000..8abd2fd
--- /dev/null
+++ b/app/src/main/java/org/xutils/common/Callback.java
@@ -0,0 +1,72 @@
+package org.xutils.common;
+
+import java.lang.reflect.Type;
+
+/**
+ * Created by wyouflf on 15/6/5.
+ * 通用回调接口
+ */
+public interface Callback {
+
+ public interface CommonCallback extends Callback {
+ void onSuccess(ResultType result);
+
+ void onError(Throwable ex, boolean isOnCallback);
+
+ void onCancelled(CancelledException cex);
+
+ void onFinished();
+ }
+
+ public interface TypedCallback extends CommonCallback {
+ Type getLoadType();
+ }
+
+ public interface CacheCallback extends CommonCallback {
+ boolean onCache(ResultType result);
+ }
+
+ public interface ProxyCacheCallback extends CacheCallback {
+ boolean onlyCache();
+ }
+
+ public interface PrepareCallback extends CommonCallback {
+ ResultType prepare(PrepareType rawData) throws Throwable;
+ }
+
+ public interface ProgressCallback extends CommonCallback {
+ void onWaiting();
+
+ void onStarted();
+
+ void onLoading(long total, long current, boolean isDownloading);
+ }
+
+ public interface GroupCallback extends Callback {
+ void onSuccess(ItemType item);
+
+ void onError(ItemType item, Throwable ex, boolean isOnCallback);
+
+ void onCancelled(ItemType item, CancelledException cex);
+
+ void onFinished(ItemType item);
+
+ void onAllFinished();
+ }
+
+ public interface Callable {
+ void call(ResultType result);
+ }
+
+ public interface Cancelable {
+ void cancel();
+
+ boolean isCancelled();
+ }
+
+ public static class CancelledException extends RuntimeException {
+ public CancelledException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+}
diff --git a/app/src/main/java/org/xutils/common/TaskController.java b/app/src/main/java/org/xutils/common/TaskController.java
new file mode 100644
index 0000000..940cee8
--- /dev/null
+++ b/app/src/main/java/org/xutils/common/TaskController.java
@@ -0,0 +1,54 @@
+package org.xutils.common;
+
+import org.xutils.common.task.AbsTask;
+
+/**
+ * Created by wyouflf on 15/6/11.
+ * 任务管理接口
+ */
+public interface TaskController {
+
+ /**
+ * 在UI线程执行runnable.
+ * 如果已在UI线程, 则直接执行.
+ */
+ void autoPost(Runnable runnable);
+
+ /**
+ * 在UI线程执行runnable.
+ * post到msg queue.
+ */
+ void post(Runnable runnable);
+
+ /**
+ * 在UI线程执行runnable.
+ *
+ * @param delayMillis 延迟时间(单位毫秒)
+ */
+ void postDelayed(Runnable runnable, long delayMillis);
+
+ /**
+ * 在后台线程执行runnable
+ */
+ void run(Runnable runnable);
+
+ /**
+ * 移除post或postDelayed提交的, 未执行的runnable
+ */
+ void removeCallbacks(Runnable runnable);
+
+ /**
+ * 开始一个异步任务
+ */
+