package in.srain.cube.image;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import in.srain.cube.app.CubeFragment;
import in.srain.cube.app.lifecycle.LifeCycleComponent;
import in.srain.cube.app.lifecycle.LifeCycleComponentManager;
import in.srain.cube.concurrent.SimpleTask;
import in.srain.cube.image.iface.*;
import in.srain.cube.util.CLog;
import in.srain.cube.util.CubeDebug;

import java.util.Iterator;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author http://www.liaohuqiu.net
 */
public class ImageLoader implements LifeCycleComponent {

    // for LoadImageTask
    private static final Object sPoolSync = new Object();
    private static LoadImageTask sTopLoadImageTask;
    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 0;

    private static final String MSG_ATTACK_TO_RUNNING_TASK = "%s attach to running: %s";

    private static final String MSG_TASK_DO_IN_BACKGROUND = "%s, %s LoadImageTask.doInBackground";
    private static final String MSG_TASK_WAITING = "%s, %s LoadImageTask.waiting";
    private static final String MSG_TASK_FINISH = "%s, %s LoadImageTask.onFinish, mExitTasksEarly? %s";
    private static final String MSG_TASK_AFTER_fetchBitmapData = "%s, %s LoadImageTask.afterFetchBitmapData, canceled? %s";
    private static final String MSG_TASK_CANCEL = "%s, %s LoadImageTask.onCancel";
    private static final String MSG_TASK_RECYCLE = "%s, %s LoadImageTask.removeAndRecycle";
    private static final String MSG_HIT_CACHE = "%s hit cache %s %s";

    protected static final boolean DEBUG = CubeDebug.DEBUG_IMAGE;
    protected static final String LOG_TAG = CubeDebug.DEBUG_IMAGE_LOG_TAG;

    protected ImageTaskExecutor mImageTaskExecutor;
    protected ImageReSizer mImageReSizer;
    protected ImageProvider mImageProvider;
    protected ImageLoadHandler mImageLoadHandler;
    protected ImageLoadProgressHandler mLoadImageLoadProgressHandler;
    protected ImageDownloader mImageDownloader;

    protected boolean mPauseWork = false;
    protected boolean mExitTasksEarly = false;

    private final Object mPauseWorkLock = new Object();
    private ConcurrentHashMap<String, LoadImageTask> mLoadWorkList;
    protected Context mContext;

    protected Resources mResources;
    protected boolean mHasBeenAddedToComponentManager = false;

    public static final int TASK_ORDER_FIRST_IN_FIRST_OUT = 1;
    public static final int TASK_ORDER_LAST_IN_FIRST_OUT = 2;

    public ImageLoader(Context context, ImageProvider imageProvider, ImageTaskExecutor imageTaskExecutor, ImageReSizer imageReSizer, ImageLoadHandler imageLoadHandler) {
        mContext = context;
        mResources = context.getResources();

        mImageProvider = imageProvider;
        mImageTaskExecutor = imageTaskExecutor;
        mImageReSizer = imageReSizer;
        mImageLoadHandler = imageLoadHandler;

        mLoadWorkList = new ConcurrentHashMap<String, LoadImageTask>();
    }

    public void setImageLoadHandler(ImageLoadHandler imageLoadHandler) {
        mImageLoadHandler = imageLoadHandler;
    }

    public ImageLoadHandler getImageLoadHandler() {
        return mImageLoadHandler;
    }

    public ImageDownloader getImageDownloader() {
        return mImageDownloader;
    }

    public void setImageDownloader(ImageDownloader imageDownloader) {
        mImageDownloader = imageDownloader;
    }

    public void setImageReSizer(ImageReSizer reSizer) {
        mImageReSizer = reSizer;
    }

    public ImageReSizer getImageReSizer() {
        return mImageReSizer;
    }

    public ImageProvider getImageProvider() {
        return mImageProvider;
    }

    /**
     * Load the image in advance.
     */
    public void preLoadImages(String[] urls) {
        int len = urls.length;
        for (int i = 0; i < len; i++) {
            ImageLoadRequest request = new ImageLoadRequest(urls[i]);
            final ImageTask imageTask = createImageTask(request);
            imageTask.setIsPreLoad();
            addImageTask(imageTask, null);
        }
    }

    /**
     * Create an ImageTask.
     * You can override this method to return a customized {@link ImageTask}.
     *
     * @param url
     * @param requestWidth
     * @param requestHeight
     * @param imageReuseInfo
     * @return
     */
    @Deprecated
    public ImageTask createImageTask(String url, int requestWidth, int requestHeight, ImageReuseInfo imageReuseInfo) {
        ImageTask imageTask = ImageTask.obtain();
        if (imageTask == null) {
            imageTask = new ImageTask();
        }
        ImageLoadRequest imageLoadRequest = new ImageLoadRequest(url, requestWidth, requestHeight, -1, imageReuseInfo);
        imageTask.renewForRequest(imageLoadRequest);
        return imageTask;
    }

    public ImageTask createImageTask(ImageLoadRequest request) {
        ImageTask imageTask = ImageTask.obtain();
        if (imageTask == null) {
            imageTask = new ImageTask();
        }

        imageTask.renewForRequest(request);
        return imageTask;
    }

    /**
     * Detach the ImageView from the ImageTask.
     *
     * @param imageTask
     * @param imageView
     */
    public void detachImageViewFromImageTask(ImageTask imageTask, CubeImageView imageView) {
        imageTask.removeImageView(imageView);
        if (imageTask.isLoading()) {
            if (!imageTask.isPreLoad() && !imageTask.stillHasRelatedImageView()) {
                LoadImageTask task = mLoadWorkList.get(imageTask.getIdentityKey());
                if (task != null) {
                    task.cancel();
                }
                if (DEBUG) {
                    CLog.d(LOG_TAG, "%s previous work is cancelled.", imageTask);
                }
            }
        }
        if (!imageTask.stillHasRelatedImageView()) {
            imageTask.tryToRecycle();
        }
    }

    /**
     * Add the ImageTask into loading list.
     *
     * @param imageTask
     * @param imageView
     */
    public void addImageTask(ImageTask imageTask, CubeImageView imageView) {
        if (!mHasBeenAddedToComponentManager) {
            CLog.w(LOG_TAG, "ImageLoader has not been add to a Component Manager.", this);
        }
        LoadImageTask runningTask = mLoadWorkList.get(imageTask.getIdentityKey());
        if (runningTask != null) {
            if (imageView != null) {
                if (DEBUG) {
                    CLog.d(LOG_TAG, MSG_ATTACK_TO_RUNNING_TASK, imageTask, runningTask.getImageTask());
                }
                runningTask.getImageTask().addImageView(imageView);
                runningTask.getImageTask().notifyLoading(mImageLoadHandler, imageView);
            }
            return;
        } else {
            imageTask.addImageView(imageView);
        }

        imageTask.onLoading(mImageLoadHandler);

        LoadImageTask loadImageTask = createLoadImageTask(imageTask);
        mLoadWorkList.put(imageTask.getIdentityKey(), loadImageTask);
        mImageTaskExecutor.execute(loadImageTask);
    }

    /**
     * Check weather this imageTask has cache Drawable data.
     */
    public boolean queryCache(ImageTask imageTask, CubeImageView imageView) {
        if (null == mImageProvider) {
            return false;
        }
        BitmapDrawable drawable = mImageProvider.getBitmapFromMemCache(imageTask);

        if (imageTask.getStatistics() != null) {
            imageTask.getStatistics().s0_afterCheckMemoryCache(drawable != null);
        }
        if (drawable == null) {
            return false;
        }

        if (DEBUG) {
            CLog.d(LOG_TAG, MSG_HIT_CACHE, imageTask, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        }
        imageTask.addImageView(imageView);
        imageTask.onLoadTaskFinish(drawable, mImageLoadHandler);
        return true;
    }

    /**
     * set task executed order: {@link @TASK_ORDER_LAST_IN_FIRST_OUT} or {@link #TASK_ORDER_FIRST_IN_FIRST_OUT}
     *
     * @param order
     */
    @SuppressWarnings({"unused"})
    public void setTaskOrder(int order) {
        if (null != mImageTaskExecutor) {
            mImageTaskExecutor.setTaskOrder(order);
        }
    }

    /**
     * flush un-cached image to disk
     */
    public void flushFileCache() {
        if (mImageProvider != null) {
            mImageProvider.flushFileCache();
        }
    }

    private LoadImageTask createLoadImageTask(ImageTask imageTask) {
        // pop top, make top.next as top
        synchronized (sPoolSync) {
            if (sTopLoadImageTask != null) {
                LoadImageTask m = sTopLoadImageTask;
                m.mNextImageTask = null;
                m.renew(this, imageTask);

                sTopLoadImageTask = m.mNextImageTask;
                sPoolSize--;
                return m;
            }
        }
        return new LoadImageTask().renew(this, imageTask);
    }

    /**
     * Inner class to process the image loading task in background threads.
     * <p/>
     * Memory required:
     * Shadow heap size: 24(Parent class, {@link SimpleTask}) + 4 * 3 = 36. Align to 40 bytes.
     * Retained heap size: 40 + 24(AtomicInteger introduced by {@link SimpleTask} = 64 bytes.
     *
     * @author http://www.liaohuqiu.net
     */
    public static class LoadImageTask extends SimpleTask {

        private ImageTask mImageTask;
        private BitmapDrawable mDrawable;
        private LoadImageTask mNextImageTask;
        private ImageLoader mImageLoader;

        public ImageTask getImageTask() {
            return mImageTask;
        }

        public LoadImageTask renew(ImageLoader imageLoader, ImageTask imageTask) {
            mImageLoader = imageLoader;
            mImageTask = imageTask;
            restart();
            return this;
        }

        @Override
        public void doInBackground() {
            if (DEBUG) {
                CLog.d(LOG_TAG, MSG_TASK_DO_IN_BACKGROUND, this, mImageTask);
            }

            if (mImageTask.getStatistics() != null) {
                mImageTask.getStatistics().s1_beginLoad();
            }
            Bitmap bitmap = null;
            // Wait here if work is paused and the task is not cancelled
            synchronized (mImageLoader.mPauseWorkLock) {
                while (mImageLoader.mPauseWork && !isCancelled()) {
                    try {
                        if (DEBUG) {
                            CLog.d(LOG_TAG, MSG_TASK_WAITING, this, mImageTask);
                        }
                        mImageLoader.mPauseWorkLock.wait();
                    } catch (InterruptedException e) {
                    }
                }
            }

            // If this task has not been cancelled by another
            // thread and the ImageView that was originally bound to this task is still bound back
            // to this task and our "exit early" flag is not set then try and fetch the bitmap from
            // the cache
            if (!isCancelled() && !mImageLoader.mExitTasksEarly && (mImageTask.isPreLoad() || mImageTask.stillHasRelatedImageView())) {
                try {
                    bitmap = mImageLoader.mImageProvider.fetchBitmapData(mImageLoader, mImageTask, mImageLoader.mImageReSizer);
                    if (DEBUG) {
                        CLog.d(LOG_TAG, MSG_TASK_AFTER_fetchBitmapData, this, mImageTask, isCancelled());
                    }
                    mDrawable = mImageLoader.mImageProvider.createBitmapDrawable(mImageLoader.mResources, bitmap);
                    mImageLoader.mImageProvider.addBitmapToMemCache(mImageTask.getIdentityKey(), mDrawable);
                } catch (Exception e) {
                    e.printStackTrace();
                } catch (OutOfMemoryError e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onFinish(boolean canceled) {
            if (DEBUG) {
                CLog.d(LOG_TAG, MSG_TASK_FINISH, this, mImageTask, mImageLoader.mExitTasksEarly);
            }
            if (mImageLoader.mExitTasksEarly) {
                return;
            }

            if (!isCancelled() && !mImageLoader.mExitTasksEarly) {
                mImageTask.onLoadTaskFinish(mDrawable, mImageLoader.mImageLoadHandler);
            }

            mImageLoader.mLoadWorkList.remove(mImageTask.getIdentityKey());
        }

        @Override
        public void onCancel() {
            if (DEBUG) {
                CLog.d(LOG_TAG, MSG_TASK_CANCEL, this, mImageTask);
            }
            mImageLoader.getImageProvider().cancelTask(mImageTask);
            mImageTask.onLoadTaskCancel();
            mImageLoader.mLoadWorkList.remove(mImageTask.getIdentityKey());
        }

        private void removeAndRecycle() {
            if (DEBUG) {
                CLog.d(LOG_TAG, MSG_TASK_RECYCLE, this, mImageTask);
            }
            // unlink
            mImageLoader = null;
            mImageTask = null;
            mDrawable = null;

            // mark top as the next of current, then push current as pop
            synchronized (sPoolSync) {
                if (sPoolSize < MAX_POOL_SIZE) {
                    mNextImageTask = sTopLoadImageTask;
                    sTopLoadImageTask = this;
                    sPoolSize++;
                }
            }
        }

        @Override
        public String toString() {
            return "[LoadImageTask" + '@' + Integer.toHexString(hashCode()) + ']';
        }
    }

    private void setPause(boolean pause) {
        synchronized (mPauseWorkLock) {
            mPauseWork = pause;
            if (!pause) {
                mPauseWorkLock.notifyAll();
            }
        }
    }

    /**
     * Temporarily hand up work, you can call this when the view is scrolling.
     */
    public void pauseWork() {
        mExitTasksEarly = false;
        setPause(true);
        if (DEBUG) {
            CLog.d(LOG_TAG, "work_status: pauseWork %s", this);
        }
    }

    /**
     * Resume the work
     */
    public void resumeWork() {
        mExitTasksEarly = false;
        setPause(false);
        if (DEBUG) {
            CLog.d(LOG_TAG, "work_status: resumeWork %s", this);
        }
    }

    /**
     * Recover the from the work list
     */
    public void recoverWork() {
        if (DEBUG) {
            CLog.d(LOG_TAG, "work_status: recoverWork %s", this);
        }
        mExitTasksEarly = false;
        setPause(false);
        Iterator<Entry<String, LoadImageTask>> it = (Iterator<Entry<String, LoadImageTask>>) mLoadWorkList.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, LoadImageTask> item = it.next();
            LoadImageTask task = item.getValue();
            task.restart();
            mImageTaskExecutor.execute(task);
        }
    }

    /**
     * Drop all the work, and leave it in the work list.
     */
    public void stopWork() {
        if (DEBUG) {
            CLog.d(LOG_TAG, "work_status: stopWork %s", this);
        }
        mExitTasksEarly = true;
        setPause(false);

        flushFileCache();
    }

    /**
     * Drop all the work, clear the work list.
     */
    public void destroy() {
        if (DEBUG) {
            CLog.d(LOG_TAG, "work_status: destroy %s", this);
        }
        mExitTasksEarly = true;
        setPause(false);

        Iterator<Entry<String, LoadImageTask>> it = (Iterator<Entry<String, LoadImageTask>>) mLoadWorkList.entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, LoadImageTask> item = it.next();
            final LoadImageTask task = item.getValue();
            it.remove();
            if (task != null) {
                task.cancel();
            }
        }
        mLoadWorkList.clear();
    }

    /**
     * The UI becomes partially invisible.
     * like {@link android.app.Activity#onPause}
     */
    @Override
    public void onBecomesPartiallyInvisible() {
        pauseWork();
    }

    /**
     * The UI becomes visible from partially invisible.
     * like {@link android.app.Activity#onResume}
     */
    @Override
    public void onBecomesVisible() {
        resumeWork();
    }

    /**
     * The UI becomes totally invisible.
     * like {@link android.app.Activity#onStop}
     */
    @Override
    public void onBecomesTotallyInvisible() {
        stopWork();
    }

    /**
     * The UI becomes visible from totally invisible.
     * like {@link android.app.Activity#onRestart}
     */
    @Override
    public void onBecomesVisibleFromTotallyInvisible() {
        recoverWork();
    }

    /**
     * like {@link android.app.Activity#onDestroy}
     */
    @Override
    public void onDestroy() {
        destroy();
    }

    /**
     * try to attach to {@link in.srain.cube.app.lifecycle.IComponentContainer}
     */
    public ImageLoader tryToAttachToContainer(Object object) {
        tryToAttachToContainer(object, true);
        return this;
    }

    /**
     * try to attach to {@link in.srain.cube.app.lifecycle.IComponentContainer}
     */
    public ImageLoader tryToAttachToContainer(Object object, boolean throwEx) {
        if (LifeCycleComponentManager.tryAddComponentToContainer(this, object, throwEx)) {
            mHasBeenAddedToComponentManager = true;
        }
        return this;
    }

    /**
     * LiefCycle phase will be same to CubeFragment, an will be processed automatically.
     *
     * @param fragment
     * @return
     */
    public ImageLoader attachToCubeFragment(CubeFragment fragment) {
        if (fragment != null) {
            if (LifeCycleComponentManager.tryAddComponentToContainer(this, fragment, true)) {
                mHasBeenAddedToComponentManager = true;
            }
        }
        return this;
    }
}


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) {
            if (DEBUG) {
                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) {
                if (DEBUG) {
                    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 ICacheAble#onNoCacheData} will not no be called.
     *
     * @param cacheAble
     * @param <T>
     * @return if not cache data available, return null, {@link 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);
            }
        }
    }
}


package in.srain.cube.set.hash;

import java.util.*;

public class SimpleHashSet<T> extends AbstractSet<T> implements Set<T>, Cloneable {

    private static final int MINIMUM_CAPACITY = 4;
    private static final int MAXIMUM_CAPACITY = 1 << 30;

    private static final SimpleHashSetEntry[] EMPTY_TABLE = new SimpleHashSetEntry[MINIMUM_CAPACITY >>> 1];
    transient SimpleHashSetEntry<T>[] mTable;
    transient int mSize;
    private transient int threshold;
    private SimpleHashSetEntry<T> mEntryForNull;

    public SimpleHashSet() {
        mTable = EMPTY_TABLE;
        // Forces first put invocation to replace EMPTY_TABLE
        threshold = -1;
    }

    public SimpleHashSet(int capacity) {
        if (capacity < 0) {
            throw new IllegalArgumentException("Capacity: " + capacity);
        }

        if (capacity == 0) {
            SimpleHashSetEntry<T>[] tab = EMPTY_TABLE;
            mTable = tab;
            threshold = -1; // Forces first put() to replace EMPTY_TABLE
            return;
        }

        if (capacity < MINIMUM_CAPACITY) {
            capacity = MINIMUM_CAPACITY;
        } else if (capacity > MAXIMUM_CAPACITY) {
            capacity = MAXIMUM_CAPACITY;
        } else {
            capacity = roundUpToPowerOfTwo(capacity);
        }
        makeTable(capacity);
    }

    public SimpleHashSet(Collection<? extends T> collection) {
        this(collection.size() < 6 ? 11 : collection.size() * 2);
        for (T e : collection) {
            add(e);
        }
    }

    public static int roundUpToPowerOfTwo(int i) {
        // If input is a power of two, shift its high-order bit right.
        i--;

        // "Smear" the high-order bit all the way to the right.
        i |= i >>> 1;
        i |= i >>> 2;
        i |= i >>> 4;
        i |= i >>> 8;
        i |= i >>> 16;

        return i + 1;
    }

    public static int secondaryHash(Object key) {
        int hash = key.hashCode();
        hash ^= (hash >>> 20) ^ (hash >>> 12);
        hash ^= (hash >>> 7) ^ (hash >>> 4);
        return hash;
    }

    @Override
    public Iterator<T> iterator() {
        return new HashSetIterator();
    }

    @Override
    public int size() {
        return mSize;
    }

    @Override
    public boolean remove(Object key) {
        if (key == null) {
            if (mEntryForNull == null) {
                return false;
            } else {
                mEntryForNull = null;
                mSize--;
                return true;
            }
        }
        int hash = secondaryHash(key);
        SimpleHashSetEntry<T>[] tab = mTable;
        int index = hash & (tab.length - 1);
        for (SimpleHashSetEntry<T> e = tab[index], prev = null; e != null; prev = e, e = e.mNext) {
            if (e.mHash == hash && key.equals(e.mKey)) {
                if (prev == null) {
                    tab[index] = e.mNext;
                } else {
                    prev.mNext = e.mNext;
                }
                mSize--;
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean add(T key) {
        if (key == null) {
            if (mEntryForNull == null) {
                mSize++;
                mEntryForNull = new SimpleHashSetEntry<T>(0, null);
                return true;
            }
            return false;
        }

        int hash = secondaryHash(key);
        SimpleHashSetEntry<T>[] tab = mTable;
        int index = hash & (tab.length - 1);
        for (SimpleHashSetEntry<T> e = tab[index]; e != null; e = e.mNext) {
            if (e.mKey == key || (e.mHash == hash && e.mKey.equals(key))) {
                return false;
            }
        }

        // No entry for (non-null) key is present; create one
        if (mSize++ > threshold) {
            tab = doubleCapacity();
            index = hash & (tab.length - 1);
        }
        tab[index] = new SimpleHashSetEntry<T>(hash, key);
        return true;
    }

    /**
     * Allocate a table of the given capacity and set the threshold accordingly.
     *
     * @param newCapacity must be a power of two
     */
    private SimpleHashSetEntry<T>[] makeTable(int newCapacity) {
        @SuppressWarnings("unchecked")
        SimpleHashSetEntry<T>[] newTable = (SimpleHashSetEntry<T>[]) new SimpleHashSetEntry[newCapacity];
        mTable = newTable;
        threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity
        return newTable;
    }

    /**
     * Doubles the capacity of the hash table. Existing entries are placed in
     * the correct bucket on the enlarged table. If the current capacity is,
     * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which
     * will be new unless we were already at MAXIMUM_CAPACITY.
     */
    private SimpleHashSetEntry<T>[] doubleCapacity() {
        SimpleHashSetEntry<T>[] oldTable = mTable;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            return oldTable;
        }
        int newCapacity = oldCapacity * 2;
        SimpleHashSetEntry<T>[] newTable = makeTable(newCapacity);
        if (mSize == 0) {
            return newTable;
        }

        for (int j = 0; j < oldCapacity; j++) {
            /*
             * Rehash the bucket using the minimum number of field writes.
             * This is the most subtle and delicate code in the class.
             */
            SimpleHashSetEntry<T> e = oldTable[j];
            if (e == null) {
                continue;
            }
            int highBit = e.mHash & oldCapacity;
            SimpleHashSetEntry<T> broken = null;
            newTable[j | highBit] = e;
            for (SimpleHashSetEntry<T> n = e.mNext; n != null; e = n, n = n.mNext) {
                int nextHighBit = n.mHash & oldCapacity;
                if (nextHighBit != highBit) {
                    if (broken == null) {
                        newTable[j | nextHighBit] = n;
                    } else {
                        broken.mNext = n;
                    }
                    broken = e;
                    highBit = nextHighBit;
                }
            }
            if (broken != null)
                broken.mNext = null;
        }
        return newTable;
    }

    @Override
    public boolean contains(Object key) {
        if (key == null) {
            return mEntryForNull != null;
        }
        int hash = secondaryHash(key);
        SimpleHashSetEntry<T>[] tab = mTable;
        for (SimpleHashSetEntry<T> e = tab[hash & (tab.length - 1)]; e != null; e = e.mNext) {
            if (e.mKey == key || (e.mHash == hash && e.mKey.equals(key))) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void clear() {
        if (mSize != 0) {
            Arrays.fill(mTable, null);
            mEntryForNull = null;
            mSize = 0;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public Object clone() {
        /*
         * This could be made more efficient. It unnecessarily hashes all of
         * the elements in the map.
         */
        SimpleHashSet<T> result;
        try {
            result = (SimpleHashSet<T>) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }

        // Restore clone to empty state, retaining our capacity and threshold
        result.mEntryForNull = null;
        result.makeTable(mTable.length);
        result.mSize = 0;

        Iterator<T> it = iterator();
        while (it.hasNext()) {
            result.add(it.next());
        }
        return result;
    }

    private static class SimpleHashSetEntry<T> {

        private int mHash;
        private T mKey;
        private SimpleHashSetEntry<T> mNext;

        private SimpleHashSetEntry(int hash, T key) {
            mHash = hash;
            mKey = key;
        }
    }

    private class HashSetIterator implements Iterator<T> {

        int nextIndex;
        SimpleHashSetEntry<T> nextEntry = mEntryForNull;
        SimpleHashSetEntry<T> lastEntryReturned;

        private HashSetIterator() {
            if (mEntryForNull == null) {
                SimpleHashSetEntry<T>[] tab = mTable;
                SimpleHashSetEntry<T> next = null;
                while (next == null && nextIndex < tab.length) {
                    next = tab[nextIndex++];
                }
                nextEntry = next;
            }
        }

        @Override
        public boolean hasNext() {
            return nextEntry != null;
        }

        @Override
        public T next() {

            if (nextEntry == null) {
                throw new NoSuchElementException();
            }

            SimpleHashSetEntry<T> entryToReturn = nextEntry;
            SimpleHashSetEntry<T>[] tab = mTable;
            SimpleHashSetEntry<T> next = entryToReturn.mNext;
            while (next == null && nextIndex < tab.length) {
                next = tab[nextIndex++];
            }
            nextEntry = next;
            lastEntryReturned = entryToReturn;
            return entryToReturn.mKey;
        }

        @Override
        public void remove() {
            if (lastEntryReturned == null) {
                throw new IllegalStateException();
            }
            SimpleHashSet.this.remove(lastEntryReturned.mKey);
            lastEntryReturned = null;
        }
    }
}


package in.srain.cube.image;

import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import in.srain.cube.cache.DiskCacheProvider;
import in.srain.cube.diskcache.CacheEntry;
import in.srain.cube.diskcache.DiskCache;
import in.srain.cube.diskcache.lru.SimpleDiskLruCache;
import in.srain.cube.image.iface.ImageDownloader;
import in.srain.cube.image.impl.SimpleDownloader;
import in.srain.cube.util.CLog;
import in.srain.cube.util.CubeDebug;

import java.io.*;

/**
 * This class handles disk and memory caching of bitmaps.
 *

 * Most of the code is taken from the Android best practice of displaying Bitmaps Displaying Bitmaps Efficiently.
 *
 * @author http://www.liaohuqiu.net
 */
public class ImageDiskCacheProvider extends DiskCacheProvider {

    protected static final boolean DEBUG = CubeDebug.DEBUG_IMAGE;
    protected static final String LOG_TAG = CubeDebug.DEBUG_IMAGE_LOG_TAG_PROVIDER;

    // 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;

    public ImageDiskCacheProvider(DiskCache diskCache) {
        super(diskCache);
    }

    public static ImageDiskCacheProvider createLru(long size, File path) {
        SimpleDiskLruCache simpleDiskLruCache = new SimpleDiskLruCache(path, 1, size);
        ImageDiskCacheProvider provider = new ImageDiskCacheProvider(simpleDiskLruCache);
        return provider;
    }

    public long getSize(String key) {
        if (!mDiskCache.has(key)) {
            return -1;
        }
        try {
            CacheEntry cacheEntry = mDiskCache.getEntry(key);
            return cacheEntry.getSize();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return -1;
    }

    public FileInputStream getInputStream(String key) {
        if (!mDiskCache.has(key)) {
            return null;
        }
        try {
            CacheEntry cacheEntry = mDiskCache.getEntry(key);
            return (FileInputStream) cacheEntry.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public FileInputStream downloadAndGetInputStream(ImageDownloader imageDownloader, ImageTask imageTask, String fileCacheKey, String url) {
        if (imageDownloader == null) {
            imageDownloader = SimpleDownloader.getInstance();
        }
        try {
            CacheEntry cacheEntry = mDiskCache.beginEdit(fileCacheKey);
            if (cacheEntry != null) {
                OutputStream outputStream = cacheEntry.newOutputStream();
                boolean ret = imageDownloader.downloadToStream(imageTask, url, outputStream, null);
                if (DEBUG) {
                    CLog.i(LOG_TAG, "download: %s %s %s", ret, fileCacheKey, url);
                }
                if (ret) {
                    cacheEntry.commit();
                    InputStream inputStream = cacheEntry.getInputStream();
                    if (inputStream instanceof FileInputStream) {
                        return (FileInputStream) inputStream;
                    }
                } else {
                    cacheEntry.abortEdit();
                }
            }
        } catch (IOException e) {
            if (CubeDebug.DEBUG_IMAGE) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 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;
        }

        // Add to disk cache
        OutputStream out = null;
        try {
            CacheEntry cacheEntry = mDiskCache.beginEdit(key);
            if (cacheEntry != null) {
                out = cacheEntry.newOutputStream();
                bitmap.compress(DEFAULT_COMPRESS_FORMAT, DEFAULT_COMPRESS_QUALITY, out);
                cacheEntry.commit();
                out.close();
            }
        } catch (final IOException e) {
            CLog.e(LOG_TAG, "addBitmapToCache - " + e);
        } catch (Exception e) {
            CLog.e(LOG_TAG, "addBitmapToCache - " + e);
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
            } catch (IOException e) {
            }
        }
    }
}


package in.srain.cube.cache;

import android.content.Context;
import android.text.TextUtils;
import in.srain.cube.Cube;

public class CacheManagerFactory {

    private static CacheManager sDefault;

    private static final String DEFAULT_CACHE_DIR = "cube-default-cache";
    private static final int DEFAULT_CACHE_MEMORY_CACHE_SIZE = 1024;    // 1M
    private static final int DEFAULT_CACHE_DISK_CACHE_SIZE = 1024 * 10; // 10M

    public static CacheManager getDefault() {
        if (sDefault == null) {
            initDefaultCache(Cube.getInstance().getContext(), DEFAULT_CACHE_DIR, DEFAULT_CACHE_MEMORY_CACHE_SIZE, DEFAULT_CACHE_DISK_CACHE_SIZE);
        }
        return sDefault;
    }

    public static void initDefaultCache(Context content, String cacheDir, int memoryCacheSizeInKB, int fileCacheSizeInKB) {
        if (TextUtils.isEmpty(cacheDir)) {
            cacheDir = DEFAULT_CACHE_DIR;
        }
        if (memoryCacheSizeInKB <= 0) {
            memoryCacheSizeInKB = DEFAULT_CACHE_MEMORY_CACHE_SIZE;
        }

        if (fileCacheSizeInKB <= 0) {
            fileCacheSizeInKB = DEFAULT_CACHE_DISK_CACHE_SIZE;
        }
        sDefault = new CacheManager(content, cacheDir, memoryCacheSizeInKB, fileCacheSizeInKB);
    }
}


package in.srain.cube.diskcache;

import java.io.*;
import java.nio.charset.Charset;

public class CacheEntry {

    private static final Charset UTF_8 = Charset.forName("UTF-8");

    private final String mKey;
    private DiskCache mDiskCache;
    private long mOldSize;
    private long mSize;
    private boolean mIsUnderEdit;
    private boolean hasErrors;

    public CacheEntry(DiskCache diskCache, String key) {
        mDiskCache = diskCache;
        this.mKey = key;
    }

    private static String inputStreamToString(InputStream in) throws IOException {
        return FileUtils.readFully(new InputStreamReader(in, UTF_8));
    }

    public boolean isUnderEdit() {
        return mIsUnderEdit;
    }

    public String getKey() {
        return mKey;
    }

    public File getCacheFile() {
        return new File(mDiskCache.getDirectory(), mKey);
    }

    public long getSize() {
        return mSize;
    }

    public void setSize(long size) {
        mSize = size;
    }

    public long getLastSize() {
        return mOldSize;
    }

    public File getTempFile() {
        return new File(mDiskCache.getDirectory(), mKey + ".tmp");
    }

    public boolean isReadable() {
        return getCacheFile().exists();
    }

    /**
     * Returns an unbuffered input stream to read the last committed value,
     * or null if no value has been committed.
     */
    public InputStream getInputStream() throws IOException {
        synchronized (mDiskCache) {
            if (!isReadable()) {
                return null;
            }
            return new FileInputStream(getCacheFile());
        }
    }

    /**
     * Returns the last committed value as a string, or null if no value
     * has been committed.
     */
    public String getString() throws IOException {
        InputStream in = getInputStream();
        return in != null ? inputStreamToString(in) : null;
    }

    /**
     * Sets the value
     */
    public CacheEntry setString(String value) throws IOException {
        Writer writer = null;
        try {
            writer = new OutputStreamWriter(newOutputStream(), UTF_8);
            writer.write(value);
        } finally {
            FileUtils.closeQuietly(writer);
        }
        return this;
    }

    /**
     * Returns a new unbuffered output stream to write the value
     * If the underlying output stream encounters errors
     * when writing to the filesystem, this edit will be aborted when
     * {@link #commit} is called. The returned output stream does not throw
     * IOExceptions.
     */
    public OutputStream newOutputStream() throws IOException {
        synchronized (mDiskCache) {
            if (mIsUnderEdit) {
                throw new IOException("This file has been under edit");
            }
            mIsUnderEdit = true;
            File tempFile = getTempFile();
            File parent = tempFile.getParentFile();
            if (!parent.exists() && !parent.mkdirs()) {
                throw new IOException("Can not make sure the parent directory exist.");
            }
            return new FaultHidingOutputStream(new FileOutputStream(tempFile));
        }
    }

    /**
     * delete all content
     *
     * @return
     */
    public boolean delete() throws IOException {
        if (mIsUnderEdit) {
            throw new IOException("Try to delete an cache entry that has been being editing.");
        }
        FileUtils.deleteIfExists(getCacheFile());
        FileUtils.deleteIfExists(getTempFile());
        return true;
    }

    /**
     * Commits this edit so it is visible to readers.  This releases the
     * edit lock so another edit may be started on the same key.
     */
    public void commit() throws IOException {
        if (!mIsUnderEdit) {
            throw new IOException("CacheEntry has been closed.");
        }
        if (hasErrors) {
            mDiskCache.delete(mKey);
        } else {
            File dirty = getTempFile();
            if (dirty.exists()) {
                File clean = getCacheFile();
                dirty.renameTo(clean);
                mOldSize = mSize;
                mSize = clean.length();
                mDiskCache.commitEdit(this);
            } else {
                abortEdit();
            }
        }
        mIsUnderEdit = false;
    }

    /*
     * Aborts this edit. This releases the edit lock so another edit may be
     * started on the same mKey.
     */
    public synchronized void abortEdit() throws IOException {
        if (!mIsUnderEdit) {
            return;
        }
        mIsUnderEdit = false;
        FileUtils.deleteIfExists(getTempFile());
        mDiskCache.abortEdit(this);
    }

    private class FaultHidingOutputStream extends FilterOutputStream {
        private FaultHidingOutputStream(OutputStream out) {
            super(out);
        }

        @Override
        public void write(int oneByte) {
            try {
                out.write(oneByte);
            } catch (IOException e) {
                hasErrors = true;
            }
        }

        @Override
        public void write(byte[] buffer, int offset, int length) {
            try {
                out.write(buffer, offset, length);
            } catch (IOException e) {
                hasErrors = true;
            }
        }

        @Override
        public void close() {
            try {
                out.close();
            } catch (IOException e) {
                hasErrors = true;
            }
        }

        @Override
        public void flush() {
            try {
                out.flush();
            } catch (IOException e) {
                hasErrors = true;
            }
        }
    }
}


package in.srain.cube.request;

import android.text.TextUtils;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class RequestData {

    private static final String EMPTY = "";
    private static final String CHAR_QM = "?";
    private static final String CHAR_AND = "&";
    private static final String CHAR_EQ = "=";
    private static final String CHAR_SET = "UTF-8";

    public String mUrl;

    private HashMap<String, Object> mQueryData;
    private HashMap<String, Object> mPostData;
    private HashMap<String, Object> mHeaderData;
    private HashMap<String, UploadFileInfo> mUploadFileInfoHashMap;
    private boolean mUsePost = false;
    private String mTag;

    public static String buildQueryString(Map<String, ?> data, String url) {

        if (data == null || data.size() == 0) {
            return url;
        }

        StringBuilder sb = new StringBuilder();
        boolean append = false;
        if (url != null) {
            sb.append(url);
            if (url.contains(CHAR_QM)) {
                append = true;
            } else {
                sb.append(CHAR_QM);
            }
        }
        Iterator<? extends Map.Entry<String, ?>> it = data.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, ?> item = it.next();
            if (append) {
                sb.append(CHAR_AND);
            } else {
                append = true;
            }

            try {
                if (TextUtils.isEmpty(item.getKey())) {
                    continue;
                }
                sb.append(URLEncoder.encode(item.getKey(), CHAR_SET));
                sb.append(CHAR_EQ);
                if (item.getValue() != null) {
                    sb.append(URLEncoder.encode(item.getValue().toString(), CHAR_SET));
                }
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
        }
        return sb.toString();
    }

    public RequestData addPostData(String key, Object data) {
        if (mPostData == null) {
            mPostData = new HashMap<String, Object>();
        }
        mPostData.put(key, data);
        return this;
    }

    public RequestData addHeader(String key, Object data) {
        if (mHeaderData == null) {
            mHeaderData = new HashMap<String, Object>();
        }
        mHeaderData.put(key, data);
        return this;
    }

    public RequestData addPostData(Map<String, ?> data) {
        if (mPostData == null) {
            mPostData = new HashMap<String, Object>();
        }
        mPostData.putAll(data);
        return this;
    }

    public RequestData addQueryData(String key, Object data) {
        if (mQueryData == null) {
            mQueryData = new HashMap<String, Object>();
        }
        mQueryData.put(key, data);
        return this;
    }

    public RequestData addQueryData(Map<String, ?> data) {
        if (mQueryData == null) {
            mQueryData = new HashMap<String, Object>();
        }
        mQueryData.putAll(data);
        return this;
    }

    public String getRequestUrl() {
        if (mQueryData != null) {
            return buildQueryString(mQueryData, mUrl);
        }
        return mUrl;
    }

    public RequestData setRequestUrl(String url) {
        mUrl = url;
        return this;
    }

    public HashMap<String, Object> getHeaderData() {
        return mHeaderData;
    }

    public HashMap<String, Object> getQueryData() {
        return mQueryData;
    }

    public HashMap<String, Object> getPostData() {
        return mPostData;
    }

    public RequestData usePost(boolean use) {
        mUsePost = use;
        return this;
    }

    /**
     * add file to be uploaded
     *
     * @param fieldName
     * @param uploadFile, will extract extension from the file name
     * @return
     */
    @SuppressWarnings({"unused"})
    public RequestData addFile(String fieldName, String uploadFile) {
        addFile(fieldName, uploadFile, null);
        return this;
    }

    /**
     * add file to be uploaded
     *
     * @param fieldName
     * @param uploadFile
     * @param fileName   if provided, will use this as filename
     * @return
     */
    @SuppressWarnings({"unused"})
    public RequestData addFile(String fieldName, String uploadFile, String fileName) {
        addFile(fieldName, new File(uploadFile), fileName);
        return this;
    }

    /**
     * @param fieldName
     * @param uploadFile
     * @return
     */
    @SuppressWarnings({"unused"})
    public RequestData addFile(String fieldName, File uploadFile) {
        addFile(fieldName, uploadFile, null);
        return this;
    }

    @SuppressWarnings({"unused"})
    public RequestData addFile(String fieldName, File uploadFile, String fileName) {

        if (mUploadFileInfoHashMap == null) {
            mUploadFileInfoHashMap = new HashMap<String, UploadFileInfo>();
        }

        UploadFileInfo uploadFileInfo = new UploadFileInfo();
        uploadFileInfo.fieldName = fieldName;
        uploadFileInfo.uploadFile = uploadFile;
        uploadFileInfo.fileName = fileName;

        mUploadFileInfoHashMap.put(fieldName, uploadFileInfo);
        return this;
    }

    public String getTag() {
        return mTag;
    }

    /**
     * Set a tag to mark this request
     */
    public RequestData setTag(String tag) {
        mTag = tag;
        return this;
    }

    public String getPostString() {
        if (mPostData == null || mPostData.size() == 0) {
            return EMPTY;
        }
        return buildQueryString(mPostData, null);
    }

    public boolean shouldPost() {
        return mUsePost || (mPostData != null && mPostData.size() > 0) || isMultiPart();
    }

    public HashMap<String, UploadFileInfo> getUploadFiles() {
        return mUploadFileInfoHashMap;
    }

    public boolean isMultiPart() {
        return mUploadFileInfoHashMap != null && mUploadFileInfoHashMap.size() > 0;
    }

    @Override
    public String toString() {
        return String.format("RequestData: [%s, G: %s, P: %s, F: %s]", getRequestUrl(), mQueryData, mPostData, mUploadFileInfoHashMap);
    }

    public static class UploadFileInfo {
        public File uploadFile;
        public String fileName;
        public String fieldName;

        @Override
        public String toString() {
            return String.format("UploadFileInfo:[%s %s %s]", fieldName, fileName, uploadFile);
        }
    }
}


package in.srain.cube.image.impl;

import android.graphics.drawable.BitmapDrawable;
import android.support.v4.util.LruCache;
import android.util.Log;
import in.srain.cube.image.ImageProvider;
import in.srain.cube.image.drawable.RecyclingBitmapDrawable;
import in.srain.cube.image.iface.ImageMemoryCache;
import in.srain.cube.util.CubeDebug;

public class DefaultMemoryCache implements ImageMemoryCache {

    protected static final boolean DEBUG = CubeDebug.DEBUG_IMAGE;
    protected static final String LOG_TAG = CubeDebug.DEBUG_IMAGE_LOG_TAG_PROVIDER;
    private LruCache<String, BitmapDrawable> mMemoryCache;

    public DefaultMemoryCache(int cacheSizeInKB) {

        // Set up memory cache
        if (DEBUG) {
            Log.d(LOG_TAG, "Memory cache created (size = " + cacheSizeInKB + " KB)");
        }

        mMemoryCache = new LruCache<String, BitmapDrawable>(cacheSizeInKB) {

            /**
             * Notify the removed entry that is no longer being cached
             */
            @Override
            protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) {
                if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
                    // The removed entry is a recycling drawable, so notify it
                    // that it has been removed from the memory cache
                    ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
                } else {
                    // The removed entry is a standard BitmapDrawable
                    // do nothing
                }
            }

            /**
             * Measure item size in kilobytes rather than units which is more practical for a bitmap cache
             */
            @Override
            protected int sizeOf(String key, BitmapDrawable value) {
                final int bitmapSize = (int) (ImageProvider.getBitmapSize(value) / 1024);
                return bitmapSize == 0 ? 1 : bitmapSize;
            }
        };
    }

    @Override
    public void set(String key, BitmapDrawable drawable) {
        if (key == null || drawable == null) {
            return;
        }

        // Add to memory cache
        if (mMemoryCache != null) {
            if (RecyclingBitmapDrawable.class.isInstance(drawable)) {
                // The removed entry is a recycling drawable, so notify it
                // that it has been added into the memory cache
                ((RecyclingBitmapDrawable) drawable).setIsCached(true);
            }
            mMemoryCache.put(key, drawable);
        }
    }

    /**
     * Get from memory cache.
     */
    @Override
    public BitmapDrawable get(String key) {
        BitmapDrawable memValue = null;
        if (mMemoryCache != null) {
            memValue = mMemoryCache.get(key);
        }
        return memValue;
    }

    /**
     * clear the memory cache
     */
    @Override
    public void clear() {
        if (mMemoryCache != null) {
            mMemoryCache.evictAll();
            if (DEBUG) {
                Log.d(LOG_TAG, "Memory cache cleared");
            }
        }
    }

    @Override
    public void delete(String key) {
        mMemoryCache.remove(key);
    }

    @Override
    public long getMaxSize() {
        return mMemoryCache.maxSize() * 1024;
    }

    @Override
    public long getUsedSpace() {
        return mMemoryCache.size() * 1024;
    }
}


/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package in.srain.cube.diskcache.lru;

import android.util.Log;
import in.srain.cube.diskcache.CacheEntry;
import in.srain.cube.diskcache.DiskCache;
import in.srain.cube.diskcache.FileUtils;
import in.srain.cube.set.hash.SimpleHashSet;
import in.srain.cube.util.CLog;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;

public final class LruActionTracer implements Runnable {

    final static int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TMP = "journal.tmp";
    static final String MAGIC = "lru-tracer";
    static final String VERSION_1 = "1";

    private static final byte ACTION_CLEAN = 1;
    private static final byte ACTION_DIRTY = 2;
    private static final byte ACTION_DELETE = 3;
    private static final byte ACTION_READ = 4;
    private static final byte ACTION_PENDING_DELETE = 5;
    private static final byte ACTION_FLUSH = 6;

    private static final String[] sACTION_LIST = new String[]{"UN_KNOW", "CLEAN", "DIRTY", "DELETE", "READ", "DELETE_PENDING", "FLUSH"};

    private static final int IO_BUFFER_SIZE = 8 * 1024;

    private static final byte[] sPoolSync = new byte[0];
    private static final int MAX_POOL_SIZE = 50;
    private static ActionMessage sPoolHeader;
    private static int sPoolSize = 0;
    private final LinkedHashMap<String, CacheEntry> mLruEntries
            = new LinkedHashMap<String, CacheEntry>(0, 0.75f, true);
    /**
     * This cache uses a single background thread to evict entries.
     */
    private final ExecutorService mExecutorService = new ThreadPoolExecutor(0, 1,
            60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    private final File mJournalFile;
    private final File mJournalFileTmp;
    private boolean mIsRunning = false;
    private DiskCache mDiskCache;
    private long mSize = 0;
    private ConcurrentLinkedQueue<ActionMessage> mActionQueue;

    private File mDirectory;
    private long mCapacity;
    private int mAppVersion;
    private SimpleHashSet mNewCreateList;
    private Object mLock = new Object();
    private Writer mJournalWriter;
    private int mRedundantOpCount;
    private HashMap<String, CacheEntry> mEditList;

    public LruActionTracer(DiskCache diskCache, File directory, int appVersion, long capacity) {
        mDiskCache = diskCache;
        mJournalFile = new File(directory, JOURNAL_FILE);
        mJournalFileTmp = new File(directory, JOURNAL_FILE_TMP);

        mDirectory = directory;
        mAppVersion = appVersion;
        mCapacity = capacity;
        mNewCreateList = new SimpleHashSet();
        mEditList = new HashMap<String, CacheEntry>();
        mActionQueue = new ConcurrentLinkedQueue<ActionMessage>();
    }

    private static void validateKey(String key) {
        if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
            throw new IllegalArgumentException(
                    "keys must not contain spaces or newlines: \"" + key + "\"");
        }
    }

    /**
     * try to resume last status when we got off
     *
     * @throws java.io.IOException
     */
    public void tryToResume() throws IOException {
        if (mJournalFile.exists()) {
            try {
                readJournal();
                processJournal();
                mJournalWriter = new BufferedWriter(new FileWriter(mJournalFile, true),
                        IO_BUFFER_SIZE);
                if (SimpleDiskLruCache.DEBUG) {
                    CLog.d(SimpleDiskLruCache.LOG_TAG, "open success");
                }
            } catch (IOException journalIsCorrupt) {
                journalIsCorrupt.printStackTrace();
                if (SimpleDiskLruCache.DEBUG) {
                    CLog.d(SimpleDiskLruCache.LOG_TAG, "clear old cache");
                }
                clear();
            }
        } else {

            if (SimpleDiskLruCache.DEBUG) {
                CLog.d(SimpleDiskLruCache.LOG_TAG, "create new cache");
            }

            // create a new empty cache
            if (mDirectory.exists()) {
                mDirectory.delete();
            }
            mDirectory.mkdirs();
            rebuildJournal();
        }
    }

    public synchronized void clear() throws IOException {

        // abort edit
        for (CacheEntry cacheEntry : new ArrayList<CacheEntry>(mLruEntries.values())) {
            if (cacheEntry.isUnderEdit()) {
                cacheEntry.abortEdit();
            }
        }
        mLruEntries.clear();
        mSize = 0;

        // delete current directory then rebuild
        if (SimpleDiskLruCache.DEBUG) {
            CLog.d(SimpleDiskLruCache.LOG_TAG, "delete directory");
        }

        waitJobDone();

        // rebuild
        FileUtils.deleteDirectoryQuickly(mDirectory);
        rebuildJournal();
    }

    /**
     * Returns a {@link in.srain.cube.diskcache.CacheEntry} named {@code key}, or null if it doesn't
     * exist is not currently readable. If a value is returned, it is moved to
     * the head of the LRU queue.
     */
    public synchronized CacheEntry getEntry(String key) throws IOException {
        checkNotClosed();
        validateKey(key);
        CacheEntry cacheEntry = mLruEntries.get(key);
        if (cacheEntry == null) {
            return null;
        }

        trimToSize();
        addActionLog(ACTION_READ, cacheEntry);
        return cacheEntry;
    }

    public synchronized CacheEntry beginEdit(String key) throws IOException {
        checkNotClosed();
        validateKey(key);

        if (SimpleDiskLruCache.DEBUG) {
            CLog.d(SimpleDiskLruCache.LOG_TAG, "beginEdit: %s", key);
        }
        CacheEntry cacheEntry = mLruEntries.get(key);
        if (cacheEntry == null) {
            cacheEntry = new CacheEntry(mDiskCache, key);
            mNewCreateList.add(key);
            mLruEntries.put(key, cacheEntry);
        }
        mEditList.put(key, cacheEntry);

        addActionLog(ACTION_DIRTY, cacheEntry);
        return cacheEntry;
    }

    public void abortEdit(String key) {
        CacheEntry cacheEntry = mEditList.get(key);
        if (cacheEntry != null) {
            try {
                cacheEntry.abortEdit();
            } catch (IOException e) {
            }
        }
    }

    public void abortEdit(CacheEntry cacheEntry) {
        final String cacheKey = cacheEntry.getKey();
        if (SimpleDiskLruCache.DEBUG) {
            CLog.d(SimpleDiskLruCache.LOG_TAG, "abortEdit: %s", cacheKey);
        }
        if (mNewCreateList.contains(cacheKey)) {
            mLruEntries.remove(cacheKey);
            mNewCreateList.remove(cacheKey);
        }
        mEditList.remove(cacheKey);
    }

    public void commitEdit(CacheEntry cacheEntry) throws IOException {
        if (SimpleDiskLruCache.DEBUG) {
            CLog.d(SimpleDiskLruCache.LOG_TAG, "commitEdit: %s", cacheEntry.getKey());
        }

        mNewCreateList.remove(cacheEntry.getKey());
        mEditList.remove(cacheEntry.getKey());

        mSize += cacheEntry.getSize() - cacheEntry.getLastSize();
        addActionLog(ACTION_CLEAN, cacheEntry);
        trimToSize();
    }

    private void readJournalLine(String line) throws IOException {
        String[] parts = line.split(" ");
        if (parts.length < 2) {
            throw new IOException("unexpected journal line: " + line);
        }

        if (parts.length != 3) {
            throw new IOException("unexpected journal line: " + line);
        }

        String key = parts[1];
        if (parts[0].equals(sACTION_LIST[ACTION_DELETE])) {
            mLruEntries.remove(key);
            return;
        }

        CacheEntry cacheEntry = mLruEntries.get(key);
        if (cacheEntry == null) {
            cacheEntry = new CacheEntry(mDiskCache, key);
            mLruEntries.put(key, cacheEntry);
        }

        if (parts[0].equals(sACTION_LIST[ACTION_CLEAN])) {
            cacheEntry.setSize(Long.parseLong(parts[2]));
        } else if (parts[0].equals(sACTION_LIST[ACTION_DIRTY])) {
            // skip
        } else if (parts[0].equals(sACTION_LIST[ACTION_READ])) {
            // this work was already done by calling mLruEntries.get()
        } else {
            throw new IOException("unexpected journal line: " + line);
        }
    }

    /**
     * Computes the initial size and collects garbage as a part of opening the
     * cache. Dirty entries are assumed to be inconsistent and will be deleted.
     */
    private void processJournal() throws IOException {
        FileUtils.deleteIfExists(mJournalFileTmp);
        for (Iterator<CacheEntry> i = mLruEntries.values().iterator(); i.hasNext(); ) {
            CacheEntry cacheEntry = i.next();
            if (!cacheEntry.isUnderEdit()) {
                mSize += cacheEntry.getSize();
            } else {
                cacheEntry.delete();
                i.remove();
            }
        }
    }

    /**
     * Creates a new journal that omits redundant information. This replaces the
     * current journal if it exists.
     */
    private void rebuildJournal() throws IOException {
        if (mJournalWriter != null) {
            mJournalWriter.close();
        }

        Writer writer = new BufferedWriter(new FileWriter(mJournalFileTmp), IO_BUFFER_SIZE);
        writer.write(MAGIC);
        writer.write("\n");
        writer.write(VERSION_1);
        writer.write("\n");
        writer.write(Integer.toString(mAppVersion));
        writer.write("\n");
        writer.write("\n");

        for (CacheEntry cacheEntry : mLruEntries.values()) {
            if (cacheEntry.isUnderEdit()) {
                writer.write(sACTION_LIST[ACTION_DIRTY] + ' ' + cacheEntry.getKey() + " " + cacheEntry.getSize() + '\n');
            } else {
                writer.write(sACTION_LIST[ACTION_CLEAN] + ' ' + cacheEntry.getKey() + " " + cacheEntry.getSize() + '\n');
            }
        }

        writer.close();
        mJournalFileTmp.renameTo(mJournalFile);
        mJournalWriter = new BufferedWriter(new FileWriter(mJournalFile, true), IO_BUFFER_SIZE);
    }

    private void readJournal() throws IOException {
        InputStream in = new BufferedInputStream(new FileInputStream(mJournalFile), IO_BUFFER_SIZE);
        try {
            String magic = FileUtils.readAsciiLine(in);
            String version = FileUtils.readAsciiLine(in);
            String appVersionString = FileUtils.readAsciiLine(in);
            String blank = FileUtils.readAsciiLine(in);
            if (!MAGIC.equals(magic)
                    || !VERSION_1.equals(version)
                    || !Integer.toString(mAppVersion).equals(appVersionString)
                    || !"".equals(blank)) {
                throw new IOException("unexpected journal header: ["
                        + magic + ", " + version + ", " + blank + "]");
            }

            while (true) {
                try {
                    readJournalLine(FileUtils.readAsciiLine(in));
                } catch (EOFException endOfJournal) {
                    break;
                }
            }
        } finally {
            FileUtils.closeQuietly(in);
        }
    }

    private void checkNotClosed() {
        if (mJournalFile == null) {
            throw new IllegalStateException("cache is closed");
        }
    }

    /**
     * Force buffered operations to the filesystem.
     */
    public synchronized void flush() throws IOException {
        checkNotClosed();
        trimToSize();
        addActionLog(ACTION_FLUSH, null);
        waitJobDone();
    }

    private void writeActionLog(byte action, CacheEntry cacheEntry) throws IOException {
        mJournalWriter.write(sACTION_LIST[action] + ' ' + cacheEntry.getKey() + ' ' + cacheEntry.getSize() + '\n');
        mRedundantOpCount++;
        if (mRedundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD && mRedundantOpCount >= mLruEntries.size()) {
            mRedundantOpCount = 0;
            rebuildJournal();
        }
    }

    private void doJob() {
        synchronized (mLock) {
            while (!mActionQueue.isEmpty()) {
                try {
                    ActionMessage message = mActionQueue.poll();
                    doUntil(message);
                    // if every thread get a IOexception, and mLock.notify would not call. so in sometimes this will cause mLock's deadlock
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            mLock.notify();
        }
    }

    private void doUntil(ActionMessage message) throws IOException {
        final CacheEntry cacheEntry = message.mCacheEntry;
        final byte action = message.mAction;
        message.recycle();

        if (SimpleDiskLruCache.DEBUG) {
            CLog.d(SimpleDiskLruCache.LOG_TAG, "doAction: %s, key: %s",
                    sACTION_LIST[action], cacheEntry != null ? cacheEntry.getKey() : null);
        }

        switch (action) {
            case ACTION_READ:
                writeActionLog(action, cacheEntry);
                break;

            case ACTION_DIRTY:
                writeActionLog(action, cacheEntry);
                break;

            case ACTION_CLEAN:
                writeActionLog(action, cacheEntry);
                break;

            case ACTION_DELETE:
                writeActionLog(action, cacheEntry);
                break;

            case ACTION_PENDING_DELETE:
                writeActionLog(action, cacheEntry);
                if (mLruEntries.containsKey(cacheEntry.getKey())) {
                    return;
                }
                cacheEntry.delete();
                break;
            case ACTION_FLUSH:
                mJournalWriter.flush();
                break;
        }
    }

    private void waitJobDone() {
        if (SimpleDiskLruCache.DEBUG) {
            CLog.d(SimpleDiskLruCache.LOG_TAG, "waitJobDone");
        }

        // remove synchronized method , exclude this code block for dead lock digging
        synchronized (mLock) {
            if (mIsRunning) {
                while (!mActionQueue.isEmpty()) {
                    try {
                        mLock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        if (SimpleDiskLruCache.DEBUG) {
            CLog.d(SimpleDiskLruCache.LOG_TAG, "job is done");
        }
    }

    private void addActionLog(byte action, CacheEntry cacheEntry) {
        mActionQueue.add(ActionMessage.obtain(action, cacheEntry));
        if (!mIsRunning) {
            mIsRunning = true;
            mExecutorService.submit(this);
        }
    }

    public synchronized void close() throws IOException {
        if (isClosed()) {
            return; // already closed
        }
        for (CacheEntry cacheEntry : new ArrayList<CacheEntry>(mLruEntries.values())) {
            if (cacheEntry.isUnderEdit()) {
                cacheEntry.abortEdit();
            }
        }
        trimToSize();
        waitJobDone();
        rebuildJournal();
        mJournalWriter.close();
        mJournalWriter = null;
    }

    private boolean isClosed() {
        return mJournalWriter == null;
    }

    @Override
    public void run() {
        doJob();
        mIsRunning = false;
    }

    /**
     * remove files from list, delete files
     */
    private synchronized void trimToSize() {

        if (mSize > mCapacity) {
            if (SimpleDiskLruCache.DEBUG) {
                CLog.d(SimpleDiskLruCache.LOG_TAG, "should trim, current is: %s", mSize);
            }
        }
        while (mSize > mCapacity) {
            Map.Entry<String, CacheEntry> toEvict = mLruEntries.entrySet().iterator().next();
            String key = toEvict.getKey();
            CacheEntry cacheEntry = toEvict.getValue();
            mLruEntries.remove(key);

            mSize -= cacheEntry.getSize();
            addActionLog(ACTION_PENDING_DELETE, cacheEntry);
            if (SimpleDiskLruCache.DEBUG) {
                CLog.d(SimpleDiskLruCache.LOG_TAG, "pending remove: %s, size: %s, after remove total: %s", key, cacheEntry.getSize(), mSize);
            }
        }
    }

    public synchronized boolean delete(String key) throws IOException {
        if (SimpleDiskLruCache.DEBUG) {
            CLog.d(SimpleDiskLruCache.LOG_TAG, "delete: %s", key);
        }
        checkNotClosed();
        validateKey(key);
        CacheEntry cacheEntry = mLruEntries.get(key);
        if (cacheEntry == null) {
            return false;
        }

        // delete at once
        cacheEntry.delete();
        mSize -= cacheEntry.getSize();
        cacheEntry.setSize(0);
        mLruEntries.remove(key);

        addActionLog(ACTION_DELETE, cacheEntry);
        return true;
    }

    public long getSize() {
        return mSize;
    }

    public long getCapacity() {
        return mCapacity;
    }

    public File getDirectory() {
        return mDirectory;
    }

    public boolean has(String key) {
        return mLruEntries.containsKey(key) && !mNewCreateList.contains(key);
    }

    private static class ActionMessage {
        private byte mAction;
        private CacheEntry mCacheEntry;
        private ActionMessage mNext;

        public ActionMessage(byte action, CacheEntry cacheEntry) {
            mAction = action;
            mCacheEntry = cacheEntry;
        }

        public static ActionMessage obtain(byte action, CacheEntry cacheEntry) {
            synchronized (sPoolSync) {
                if (sPoolHeader != null) {
                    ActionMessage m = sPoolHeader;

                    sPoolHeader = m.mNext;
                    m.mNext = null;
                    sPoolSize--;

                    m.mAction = action;
                    m.mCacheEntry = cacheEntry;
                    return m;
                }
            }
            return new ActionMessage(action, cacheEntry);
        }

        public void recycle() {
            mAction = 0;
            mCacheEntry = null;
            synchronized (sPoolSync) {
                if (sPoolSize < MAX_POOL_SIZE) {
                    mNext = sPoolHeader;
                    sPoolHeader = this;
                    sPoolSize++;
                }
            }
        }
    }
}


package in.srain.cube.app;

import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.text.TextUtils;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import in.srain.cube.util.CLog;

public abstract class CubeFragmentActivity extends FragmentActivity {

    private final static String LOG_TAG = "cube-fragment";

    public static boolean DEBUG = true;
    protected CubeFragment mCurrentFragment;
    private boolean mCloseWarned;

    /**
     * return the string id of close warning
     * <p/>
     * return value which lower than 1 will exit instantly when press back key
     *
     * @return
     */
    protected abstract String getCloseWarning();

    protected abstract int getFragmentContainerId();

    public void pushFragmentToBackStack(Class<?> cls, Object data) {
        FragmentParam param = new FragmentParam();
        param.cls = cls;
        param.data = data;
        goToThisFragment(param);
    }

    protected String getFragmentTag(FragmentParam param) {
        StringBuilder sb = new StringBuilder(param.cls.toString());
        return sb.toString();
    }

    private void goToThisFragment(FragmentParam param) {
        int containerId = getFragmentContainerId();
        Class<?> cls = param.cls;
        if (cls == null) {
            return;
        }
        try {
            String fragmentTag = getFragmentTag(param);
            FragmentManager fm = getSupportFragmentManager();
            if (DEBUG) {
                CLog.d(LOG_TAG, "before operate, stack entry count: %s", fm.getBackStackEntryCount());
            }
            CubeFragment fragment = (CubeFragment) fm.findFragmentByTag(fragmentTag);
            if (fragment == null) {
                fragment = (CubeFragment) cls.newInstance();
            }
            if (mCurrentFragment != null && mCurrentFragment != fragment) {
                mCurrentFragment.onLeave();
            }
            fragment.onEnter(param.data);

            FragmentTransaction ft = fm.beginTransaction();
            if (fragment.isAdded()) {
                if (DEBUG) {
                    CLog.d(LOG_TAG, "%s has been added, will be shown again.", fragmentTag);
                }
                ft.show(fragment);
            } else {
                if (DEBUG) {
                    CLog.d(LOG_TAG, "%s is added.", fragmentTag);
                }
                ft.add(containerId, fragment, fragmentTag);
            }
            mCurrentFragment = fragment;

            ft.addToBackStack(fragmentTag);
            ft.commitAllowingStateLoss();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        mCloseWarned = false;
    }

    public void goToFragment(Class<?> cls, Object data) {
        if (cls == null) {
            return;
        }
        CubeFragment fragment = (CubeFragment) getSupportFragmentManager().findFragmentByTag(cls.toString());
        if (fragment != null) {
            mCurrentFragment = fragment;
            fragment.onBackWithData(data);
        }
        getSupportFragmentManager().popBackStackImmediate(cls.toString(), 0);
    }

    public void popTopFragment(Object data) {
        FragmentManager fm = getSupportFragmentManager();
        fm.popBackStackImmediate();
        if (tryToUpdateCurrentAfterPop() && mCurrentFragment != null) {
            mCurrentFragment.onBackWithData(data);
        }
    }

    public void popToRoot(Object data) {
        FragmentManager fm = getSupportFragmentManager();
        while (fm.getBackStackEntryCount() > 1) {
            fm.popBackStackImmediate();
        }
        popTopFragment(data);
    }

    /**
     * process the return back logic
     * return true if back pressed event has been processed and should stay in current view
     *
     * @return
     */
    protected boolean processBackPressed() {
        return false;
    }

    /**
     * process back pressed
     */
    @Override
    public void onBackPressed() {

        // process back for fragment
        if (processBackPressed()) {
            return;
        }

        // process back for fragment
        boolean enableBackPressed = true;
        if (mCurrentFragment != null) {
            enableBackPressed = !mCurrentFragment.processBackPressed();
        }
        if (enableBackPressed) {
            int cnt = getSupportFragmentManager().getBackStackEntryCount();
            if (cnt <= 1 && isTaskRoot()) {
                String closeWarningHint = getCloseWarning();
                if (!mCloseWarned && !TextUtils.isEmpty(closeWarningHint)) {
                    Toast toast = Toast.makeText(this, closeWarningHint, Toast.LENGTH_SHORT);
                    toast.show();
                    mCloseWarned = true;
                } else {
                    doReturnBack();
                }
            } else {
                mCloseWarned = false;
                doReturnBack();
            }
        }
    }

    private boolean tryToUpdateCurrentAfterPop() {
        FragmentManager fm = getSupportFragmentManager();
        int cnt = fm.getBackStackEntryCount();
        if (cnt > 0) {
            String name = fm.getBackStackEntryAt(cnt - 1).getName();
            Fragment fragment = fm.findFragmentByTag(name);
            if (fragment != null && fragment instanceof CubeFragment) {
                mCurrentFragment = (CubeFragment) fragment;
            }
            return true;
        }
        return false;
    }

    protected void doReturnBack() {
        int count = getSupportFragmentManager().getBackStackEntryCount();
        if (count <= 1) {
            finish();
        } else {
            getSupportFragmentManager().popBackStackImmediate();
            if (tryToUpdateCurrentAfterPop() && mCurrentFragment != null) {
                mCurrentFragment.onBack();
            }
        }
    }

    public void hideKeyboardForCurrentFocus() {
        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        if (getCurrentFocus() != null) {
            imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
        }
        imm.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0);
    }

    public void showKeyboardAtView(View view) {
        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
    }

    public void forceShowKeyboard() {
        InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);

    }

    protected void exitFullScreen() {
        getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
    }
}


package in.srain.cube.views.loadmore;

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AbsListView;
import android.widget.LinearLayout;

/**
 * @author huqiu.lhq
 */
public abstract class LoadMoreContainerBase extends LinearLayout implements LoadMoreContainer {

    private AbsListView.OnScrollListener mOnScrollListener;
    private LoadMoreUIHandler mLoadMoreUIHandler;
    private LoadMoreHandler mLoadMoreHandler;

    private boolean mIsLoading;
    private boolean mHasMore = false;
    private boolean mAutoLoadMore = true;
    private boolean mLoadError = false;

    private boolean mListEmpty = true;
    private boolean mShowLoadingForFirstPage = false;
    private View mFooterView;

    private AbsListView mAbsListView;

    public LoadMoreContainerBase(Context context) {
        super(context);
    }

    public LoadMoreContainerBase(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mAbsListView = retrieveAbsListView();
        init();
    }

    /**
     * @deprecated It's totally wrong. Use {@link #useDefaultFooter} instead.
     */
    @Deprecated
    public void useDefaultHeader() {
        useDefaultFooter();
    }

    public void useDefaultFooter() {
        LoadMoreDefaultFooterView footerView = new LoadMoreDefaultFooterView(getContext());
        footerView.setVisibility(GONE);
        setLoadMoreView(footerView);
        setLoadMoreUIHandler(footerView);
    }

    private void init() {

        if (mFooterView != null) {
            addFooterView(mFooterView);
        }

        mAbsListView.setOnScrollListener(new AbsListView.OnScrollListener() {

            private boolean mIsEnd = false;

            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {

                if (null != mOnScrollListener) {
                    mOnScrollListener.onScrollStateChanged(view, scrollState);
                }
                if (scrollState == SCROLL_STATE_IDLE) {
                    if (mIsEnd) {
                        onReachBottom();
                    }
                }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                if (null != mOnScrollListener) {
                    mOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
                }
                if (firstVisibleItem + visibleItemCount >= totalItemCount - 1) {
                    mIsEnd = true;
                } else {
                    mIsEnd = false;
                }
            }
        });
    }

    private void tryToPerformLoadMore() {
        if (mIsLoading) {
            return;
        }

        // no more content and also not load for first page
        if (!mHasMore && !(mListEmpty && mShowLoadingForFirstPage)) {
            return;
        }

        mIsLoading = true;

        if (mLoadMoreUIHandler != null) {
            mLoadMoreUIHandler.onLoading(this);
        }
        if (null != mLoadMoreHandler) {
            mLoadMoreHandler.onLoadMore(this);
        }
    }

    private void onReachBottom() {
        // if has error, just leave what it should be
        if (mLoadError) {
            return;
        }
        if (mAutoLoadMore) {
            tryToPerformLoadMore();
        } else {
            if (mHasMore) {
                mLoadMoreUIHandler.onWaitToLoadMore(this);
            }
        }
    }

    @Override
    public void setShowLoadingForFirstPage(boolean showLoading) {
        mShowLoadingForFirstPage = showLoading;
    }

    @Override
    public void setAutoLoadMore(boolean autoLoadMore) {
        mAutoLoadMore = autoLoadMore;
    }

    @Override
    public void setOnScrollListener(AbsListView.OnScrollListener l) {
        mOnScrollListener = l;
    }

    @Override
    public void setLoadMoreView(View view) {
        // has not been initialized
        if (mAbsListView == null) {
            mFooterView = view;
            return;
        }
        // remove previous
        if (mFooterView != null && mFooterView != view) {
            removeFooterView(view);
        }

        // add current
        mFooterView = view;
        mFooterView.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                tryToPerformLoadMore();
            }
        });

        addFooterView(view);
    }

    @Override
    public void setLoadMoreUIHandler(LoadMoreUIHandler handler) {
        mLoadMoreUIHandler = handler;
    }

    @Override
    public void setLoadMoreHandler(LoadMoreHandler handler) {
        mLoadMoreHandler = handler;
    }

    /**
     * page has loaded
     *
     * @param emptyResult
     * @param hasMore
     */
    @Override
    public void loadMoreFinish(boolean emptyResult, boolean hasMore) {
        mLoadError = false;
        mListEmpty = emptyResult;
        mIsLoading = false;
        mHasMore = hasMore;

        if (mLoadMoreUIHandler != null) {
            mLoadMoreUIHandler.onLoadFinish(this, emptyResult, hasMore);
        }
    }

    @Override
    public void loadMoreError(int errorCode, String errorMessage) {
        mIsLoading = false;
        mLoadError = true;
        if (mLoadMoreUIHandler != null) {
            mLoadMoreUIHandler.onLoadError(this, errorCode, errorMessage);
        }
    }

    protected abstract void addFooterView(View view);

    protected abstract void removeFooterView(View view);

    protected abstract AbsListView retrieveAbsListView();
}


package in.srain.cube.image;

import java.util.ArrayList;

public class ImageReuseInfoManger {

    private String[] mSizeList;

    /**
     * Create Reuse Info Manger, the smaller size is in the front of the size array.
     *
     * @param size new String[] { "small_180", "big_360", "big_720" }
     */
    public ImageReuseInfoManger(String[] size) {
        mSizeList = size;
    }

    /**
     * Find out the size list can be re-sued.
     *
     * @param thisSize if "small_180" is input
     * @return {"big_360", "big_720"} will be returned.
     */
    public ImageReuseInfo create(String thisSize) {
        ArrayList<String> list = new ArrayList<String>();
        boolean canBeReused = false;
        for (int i = 0; i < mSizeList.length; i++) {
            String size = mSizeList[i];

            if (!canBeReused && thisSize.equals(size)) {
                canBeReused = true;
                continue;
            }
            if (canBeReused && !thisSize.equals(size)) {
                list.add(size);
            }
        }
        if (list.size() == 0) {
            return new ImageReuseInfo(thisSize, null);
        } else {
            String[] sizeList = new String[list.size()];
            list.toArray(sizeList);
            return new ImageReuseInfo(thisSize, sizeList);
        }
    }
}