package in.srain.cube.request;

import android.text.TextUtils;
import in.srain.cube.cache.CacheManager;
import in.srain.cube.cache.CacheResultType;
import in.srain.cube.concurrent.SimpleTask;
import in.srain.cube.util.CLog;
import in.srain.cube.util.CubeDebug;

import java.net.URI;
import java.net.URISyntaxException;

public class CacheAbleRequest<T> extends RequestBase<T> implements ICacheAbleRequest<T> {

    public static enum ResultType {
        USE_CACHE_NOT_EXPIRED,
        USE_CACHE_ANYWAY,
        USE_CACHE_ON_TIMEOUT,
        USE_DATA_FROM_SERVER,
        USE_CACHE_ON_FAIL,
    }

    protected static final boolean DEBUG = CubeDebug.DEBUG_CACHE;
    protected static final String LOG_TAG = "cube-cache-request";

    private CacheAbleRequestHandler<T> mHandler;

    private T mCacheData;
    private boolean mOutOfDate;
    private String mCacheKey = null;

    private int mTimeout = 0;
    private boolean mHasTimeout = false;
    private boolean mUseCacheAnyway = false;
    private boolean mHasNotified = false;
    protected boolean mForceQueryFromServer = false;

    private String mInitDataPath;
    private boolean mDisableCache = false;
    private long mCacheTime;

    public CacheAbleRequest() {
    }

    public CacheAbleRequest(final CacheAbleRequestHandler<T> handler) {
        setCacheAbleRequestHandler(handler);
    }

    public void setCacheAbleRequestHandler(CacheAbleRequestHandler<T> handler) {
        mHandler = handler;
    }

    public void forceQueryFromServer(boolean force) {
        mForceQueryFromServer = force;
    }

    // ===========================================================
    // Override parent
    // ===========================================================
    @Override
    public void doSendRequest() {
        RequestCacheManager.getInstance().requestCache(this);
    }

    /**
     * Timeout will not be considerate
     *
     * @return
     */
    @Override
    protected T doRequestSync() {
        T data = RequestCacheManager.getInstance().requestCacheSync(this);
        if (data == null) {
            data = RequestManager.getInstance().getRequestProxy(this).requestSync(this);
        }
        return data;
    }

    /**
     * prepare request
     */
    @Override
    protected void prepareRequest() {
        RequestManager.getInstance().getRequestProxy(this).prepareRequest(this);
    }

    // ===========================================================
    // Override Interface
    // ===========================================================
    @Override
    public void setTimeout(int timeOut) {
        mTimeout = timeOut;
    }

    @Override
    public CacheAbleRequest<T> setUseCacheAnyway(boolean use) {
        mUseCacheAnyway = use;
        return this;
    }

    @Override
    public boolean useCacheAnyway() {
        return mUseCacheAnyway;
    }

    @Override
    public void onRequestSuccess(T data) {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s, onRequestSuccess", getCacheKey());
        }
        if (hasBeenCanceled()) {
            return;
        }
        if (null != mHandler) {
            mHandler.onRequestFinish(data);

            // cache data is not available or
            // cache is available and time duration not reach timeout or not always use the cache
            if (mCacheData == null || (!mHasTimeout && !mUseCacheAnyway)) {
                notifyRequestFinish(ResultType.USE_DATA_FROM_SERVER, data, false);
            } else {
                if (DEBUG) {
                    CLog.d(LOG_TAG, "%s, will not notifyRequestFinish", getCacheKey());
                }
            }
        }
    }

    @Override
    public void onRequestFail(FailData failData) {
        RequestManager.getInstance().getRequestProxy(this).onRequestFail(this, failData);
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s, onRequestFail", getCacheKey());
        }
        if (hasBeenCanceled()) {
            return;
        }
        if (null != mHandler) {
            mHandler.onRequestFail(failData);
            if (mCacheData != null && !cacheIsDisabled() && !mUseCacheAnyway) {
                notifyRequestFinish(ResultType.USE_CACHE_ON_FAIL, mCacheData, true);
            }
        }
    }

    @Override
    public void onNoCacheData(CacheManager cacheManager) {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s, onNoCacheData", getCacheKey());
        }
        if (hasBeenCanceled()) {
            return;
        }
        doQueryFromServer();
        beginTimeout();
    }

    protected void doQueryFromServer() {
        RequestManager.getInstance().getRequestProxy(this).sendRequest(this);
    }

    protected boolean cacheRequestResult() {
        return mForceQueryFromServer || !cacheIsDisabled();
    }

    @Override
    public boolean cacheIsDisabled() {
        if (mForceQueryFromServer) {
            return true;
        }
        return mDisableCache;
    }

    // ===========================================================
    // Implements Interface {@link ICacheAble}
    // ===========================================================
    @Override
    public void onCacheData(CacheResultType cacheResultType, T data, boolean outOfDate) {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s, onQueryFinish, out of date: %s", getCacheKey(), outOfDate);
        }
        if (hasBeenCanceled()) {
            return;
        }
        mCacheData = data;
        mOutOfDate = outOfDate;
        if (mHandler != null) {
            mHandler.onCacheData(data, outOfDate);

            if (mUseCacheAnyway) {
                notifyRequestFinish(ResultType.USE_CACHE_ANYWAY, data, mOutOfDate);
            } else {
                if (!outOfDate) {
                    notifyRequestFinish(ResultType.USE_CACHE_NOT_EXPIRED, data, false);
                }
            }
        }
    }

    @Override
    public long getCacheTime() {
        return mCacheTime;
    }

    @Override
    public String getCacheKey() {
        if (mCacheKey == null) {
            String cacheKey = null;
            String url = getRequestData().getRequestUrl();
            try {
                URI uri = null;
                uri = new URI(url);
                cacheKey = uri.getPath();
                if (cacheKey.startsWith("/")) {
                    cacheKey = cacheKey.substring(1);
                }
                cacheKey = cacheKey.replace("/", "-");
            } catch (URISyntaxException e) {
                if (CubeDebug.DEBUG_REQUEST) {
                    e.printStackTrace();
                }
            }
            if (TextUtils.isEmpty(cacheKey)) {
                throw new RuntimeException("Cache key is null");
            }
            mCacheKey = cacheKey;
        }
        return mCacheKey;
    }

    @Override
    public String getAssertInitDataPath() {
        return mInitDataPath;
    }

    @Override
    public T onDataFromServer(String data) {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s, onDataFromServer", getCacheKey());
        }

        T ret = super.onDataFromServer(data);

        // cache the data
        if (!TextUtils.isEmpty(data) && ret != null && cacheRequestResult()) {
            RequestCacheManager.getInstance().setCacheData(this.getCacheKey(), data);
        }
        return ret;
    }

    @Override
    public T processOriginDataFromServer(JsonData rawData) {
        rawData = RequestManager.getInstance().getRequestProxy(this).processOriginDataFromServer(this, rawData);
        return mHandler.processOriginData(rawData);
    }

    @Override
    public T processRawDataFromCache(JsonData rawData) {
        return mHandler.processOriginData(rawData);
    }

    /**
     * will only notify once
     *
     * @param type
     * @param outOfDate
     */
    private void notifyRequestFinish(ResultType type, T cacheData, boolean outOfDate) {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s, notifyRequestFinish: %s, %s", getCacheKey(), type, outOfDate);
        }
        if (mHasNotified) {
            return;
        }
        mHasNotified = true;
        mHandler.onCacheAbleRequestFinish(cacheData, type, outOfDate);
    }

    private void timeout() {
        mHasTimeout = true;
        if (mCacheData != null && mHandler != null) {
            notifyRequestFinish(ResultType.USE_CACHE_ON_TIMEOUT, mCacheData, true);
        }
    }

    private void beginTimeout() {
        if (mTimeout > 0 && mCacheData != null) {
            SimpleTask.postDelay(new Runnable() {
                @Override
                public void run() {
                    timeout();
                }
            }, mTimeout);
        }
    }

    @Override
    public CacheAbleRequest<T> setCacheKey(String cacheKey) {
        mCacheKey = cacheKey;
        return this;
    }

    @Override
    public CacheAbleRequest<T> setDisableCache(boolean disable) {
        mDisableCache = disable;
        return this;
    }

    @Override
    public CacheAbleRequest<T> setAssertInitDataPath(String path) {
        mInitDataPath = path;
        return this;
    }

    @Override
    public CacheAbleRequest<T> setCacheTime(long time) {
        mCacheTime = time;
        return this;
    }
}


package in.srain.cube.image.impl;

import android.graphics.BitmapFactory;
import in.srain.cube.image.ImageTask;
import in.srain.cube.image.iface.ImageReSizer;

/**
 * A default implementation of {@link in.srain.cube.image.iface.ImageReSizer}
 *
 * @author http://www.liaohuqiu.net
 */
public class DefaultImageReSizer implements ImageReSizer {

    private static DefaultImageReSizer sInstance;

    public static synchronized DefaultImageReSizer getInstance() {
        if (sInstance == null) {
            sInstance = new DefaultImageReSizer();
        }
        return sInstance;
    }

    @Override
    public int getInSampleSize(ImageTask imageTask) {
        int size = calculateInSampleSize(imageTask.getBitmapOriginSize().x, imageTask.getBitmapOriginSize().y, imageTask.getRequestSize().x, imageTask.getRequestSize().y);
        return size;
    }

    @Override
    public String getRemoteUrl(ImageTask imageTask) {
        return imageTask.getRemoteUrl();
    }

    /**
     * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding bitmaps using the decode* methods from {@link BitmapFactory}.
     * <p/>
     * This implementation calculates the closest inSampleSize that is a times of 2 and will result in the final decoded bitmap having a width and height equal to or larger than the requested width and height.
     */
    public static int calculateInSampleSize(int originWidth, int originHeight, int reqWidth, int reqHeight) {

        int inSampleSize = 1;

        if (reqHeight <= 0 || reqHeight <= 0 || reqHeight == Integer.MAX_VALUE || reqWidth == Integer.MAX_VALUE) {
            return inSampleSize;
        }

        if (originWidth > reqHeight || originHeight > reqWidth) {

            final int halfHeight = originWidth / 2;
            final int halfWidth = originHeight / 2;

            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
                inSampleSize *= 2;
            }
        }
        return inSampleSize;
    }
}


/*
 * Copyright (C) 2012 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.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * Class from encrypting string
 */
public class Encrypt {
    private Encrypt() {
    }

    /**
     * A hashing method that changes a string (like a URL) into a hash string
     */
    public static String md5(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    public static String bytesToHexString(byte[] bytes) {
        // http://stackoverflow.com/questions/332079
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
}


package in.srain.cube.request.sender;

import in.srain.cube.request.IRequest;
import in.srain.cube.request.RequestData;

import java.io.*;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public abstract class BaseRequestSender implements IRequestSender {

    protected final static String EMPTY_VALUE = "";
    protected HttpURLConnection mHttpURLConnection;
    protected RequestData mRequestData;
    protected IRequest<?> mRequest;

    public BaseRequestSender(IRequest<?> request, HttpURLConnection httpURLConnection) {
        mRequest = request;
        mHttpURLConnection = httpURLConnection;
        mRequestData = request.getRequestData();
    }

    @Override
    public void setup() throws IOException {
        HashMap<String, Object> data = mRequestData.getHeaderData();
        if (data == null || data.size() == 0) {
            return;
        }
        Iterator<Map.Entry<String, Object>> it = data.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Object> item = it.next();
            String key = item.getKey();
            Object value = item.getValue();
            if (value == null) {
                value = EMPTY_VALUE;
            }
            mHttpURLConnection.setRequestProperty(key, value.toString());
        }
    }

    /**
     * Completes the request and receives response from the server.
     *
     * @return a list of Strings as response in case the server returned
     * status OK, otherwise an exception is thrown.
     * @throws IOException
     */
    public void getResponse(StringBuilder sb) throws IOException {

        // checks server's status code first
        int status = mHttpURLConnection.getResponseCode();
        if (status == HttpURLConnection.HTTP_OK) {
            InputStream ips = new BufferedInputStream(mHttpURLConnection.getInputStream());
            BufferedReader reader = new BufferedReader(new InputStreamReader(ips, "UTF-8"));

            char[] buffer = new char[1024];
            int bufferLength;
            while ((bufferLength = reader.read(buffer, 0, buffer.length)) > 0) {
                sb.append(buffer, 0, bufferLength);
            }
            reader.close();
            ips.close();
            mHttpURLConnection.disconnect();
        } else {
            throw new IOException("Server returned non-OK status: " + status);
        }
    }
}


package in.srain.cube.image.impl;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.view.ViewGroup;
import in.srain.cube.image.CubeImageView;
import in.srain.cube.image.ImageTask;
import in.srain.cube.image.drawable.RoundedDrawable;
import in.srain.cube.image.iface.ImageLoadHandler;
import in.srain.cube.util.CLog;
import in.srain.cube.util.CubeDebug;
import in.srain.cube.util.Version;

/**
 * A simple implementation of {@link ImageLoadHandler}.
 * <p/>
 * This loader will put a background to ImageView when the image is loading.
 *
 * @author http://www.liaohuqiu.net
 */
public class DefaultImageLoadHandler implements ImageLoadHandler {

    private final static boolean DEBUG = CubeDebug.DEBUG_IMAGE;
    private final static String LOG_TAG = CubeDebug.DEBUG_IMAGE_LOG_TAG;
    private final static String MSG_LOADING = "%s => %s handler on loading";
    private final static String MSG_LOAD_ERROR = "%s => %s handler on load error";
    private final static String MSG_LOAD_FINISH = "%s => %s handler on load finish: %s %s %s %s";

    private Context mContext;
    private final static int DISPLAY_FADE_IN = 0x01;
    private final static int DISPLAY_ROUNDED = 0x02;

    private int mDisplayTag = DISPLAY_FADE_IN;

    private Drawable mLoadingDrawable;
    private Drawable mErrorDrawable;

    private int mLoadingColor = -1;
    private float mCornerRadius = 10;

    private boolean mResizeImageViewAfterLoad = false;

    public DefaultImageLoadHandler(Context context) {
        mContext = context;
        mLoadingDrawable = new ColorDrawable(0xfff1f1f1);
        mErrorDrawable = new ColorDrawable(Color.RED);
    }

    /**
     * If set to true, the image will fade-in once it has been loaded by the background thread.
     */
    public void setImageFadeIn(boolean fadeIn) {
        if (fadeIn) {
            mDisplayTag |= DISPLAY_FADE_IN;
        } else {
            mDisplayTag &= ~DISPLAY_FADE_IN;
        }
    }

    /**
     *
     */
    public void setImageRounded(boolean rounded, float cornerRadius) {
        if (rounded) {
            mDisplayTag |= DISPLAY_ROUNDED;
            mCornerRadius = cornerRadius;
        } else {
            mDisplayTag &= ~DISPLAY_ROUNDED;
        }
    }

    public void setResizeImageViewAfterLoad(boolean resize) {
        mResizeImageViewAfterLoad = resize;
    }

    /**
     * set the placeholder bitmap
     */
    public void setLoadingBitmap(Bitmap loadingBitmap) {
        if (Version.hasHoneycomb()) {
            mLoadingDrawable = new BitmapDrawable(mContext.getResources(), loadingBitmap);
        }
    }

    /**
     * set the placeholder Image
     *
     * @param loadingBitmap the resources id
     */
    public void setLoadingResources(int loadingBitmap) {
        setLoadingBitmap(BitmapFactory.decodeResource(mContext.getResources(), loadingBitmap));
    }

    public void setErrorBitmap(Bitmap bitmap) {
        if (Version.hasHoneycomb()) {
            mErrorDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
        }
    }

    /**
     * set the error Image
     *
     * @param resId
     */
    public void setErrorResources(int resId) {
        setErrorBitmap(BitmapFactory.decodeResource(mContext.getResources(), resId));
    }

    /**
     * set the placeholder by color
     *
     * @param color
     */
    public void setLoadingImageColor(int color) {
        mLoadingColor = color;
        mLoadingDrawable = new ColorDrawable(color);
    }

    /**
     * set the placeholder image by color string like: #ffffff
     *
     * @param colorString
     */
    public void setLoadingImageColor(String colorString) {
        setLoadingImageColor(Color.parseColor(colorString));
    }

    @Override
    public void onLoading(ImageTask imageTask, CubeImageView imageView) {
        if (imageView == null) {
            return;
        }
        if (DEBUG) {
            CLog.d(LOG_TAG, MSG_LOADING, imageTask, imageView);
        }
        if (Version.hasHoneycomb()) {
            if (mLoadingDrawable != null && imageView != null && imageView.getDrawable() != mLoadingDrawable) {
                imageView.setImageDrawable(mLoadingDrawable);
            }
        } else {
            imageView.setImageDrawable(null);
        }
    }

    @Override
    public void onLoadError(ImageTask imageTask, CubeImageView imageView, int errorCode) {
        if (DEBUG) {
            CLog.d(LOG_TAG, MSG_LOAD_ERROR, imageTask, imageView);
        }
        if (imageView != null) {
            if (Version.hasHoneycomb()) {
                if (mErrorDrawable != null && imageView != null && imageView.getDrawable() != mErrorDrawable) {
                    imageView.setImageDrawable(mErrorDrawable);
                }
            } else {
                imageView.setImageDrawable(null);
            }
            imageView.setImageDrawable(mErrorDrawable);
        }
    }

    @Override
    public void onLoadFinish(ImageTask imageTask, CubeImageView imageView, BitmapDrawable drawable) {

        if (imageView == null) {
            return;
        }

        Drawable d = drawable;
        if (drawable != null) {

            if (mResizeImageViewAfterLoad) {
                int w = drawable.getBitmap().getWidth();
                int h = drawable.getBitmap().getHeight();

                if (w > 0 && h > 0) {

                    ViewGroup.LayoutParams lyp = imageView.getLayoutParams();
                    if (lyp != null) {
                        lyp.width = w;
                        lyp.height = h;
                        imageView.setLayoutParams(lyp);
                    }
                }
            }

            // RoundedDrawable will not recycle automatically when API level is lower than 11
            if ((mDisplayTag & DISPLAY_ROUNDED) == DISPLAY_ROUNDED && Version.hasHoneycomb()) {
                d = new RoundedDrawable(drawable.getBitmap(), mCornerRadius);
            }
            if ((mDisplayTag & DISPLAY_FADE_IN) == DISPLAY_FADE_IN) {
                int loadingColor = android.R.color.transparent;
                if (mLoadingColor != -1 && (mDisplayTag & DISPLAY_ROUNDED) != DISPLAY_ROUNDED) {
                    loadingColor = mLoadingColor;
                }
                final TransitionDrawable td = new TransitionDrawable(new Drawable[]{new ColorDrawable(loadingColor), d});
                imageView.setImageDrawable(td);
                td.startTransition(200);
            } else {

                if (DEBUG) {
                    Drawable oldDrawable = imageView.getDrawable();
                    int w = 0, h = 0;
                    if (oldDrawable != null) {
                        w = oldDrawable.getIntrinsicWidth();
                        h = oldDrawable.getIntrinsicHeight();
                    }
                    CLog.d(LOG_TAG, MSG_LOAD_FINISH,
                            imageTask, imageView, w, h, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
                }
                imageView.setImageDrawable(drawable);
            }
        }
    }
}


package in.srain.cube.mints.base;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import in.srain.cube.R;

public class TitleHeaderBar extends RelativeLayout {

    private TextView mCenterTitleTextView;
    private ImageView mLeftReturnImageView;
    private RelativeLayout mLeftViewContainer;
    private RelativeLayout mRightViewContainer;
    private RelativeLayout mCenterViewContainer;

    private String mTitle;

    public TitleHeaderBar(Context context) {
        this(context, null);
    }

    public TitleHeaderBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TitleHeaderBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        LayoutInflater.from(context).inflate(getHeaderViewLayoutId(), this);
        mLeftViewContainer = (RelativeLayout) findViewById(R.id.ly_title_bar_left);
        mCenterViewContainer = (RelativeLayout) findViewById(R.id.ly_title_bar_center);
        mRightViewContainer = (RelativeLayout) findViewById(R.id.ly_title_bar_right);
        mLeftReturnImageView = (ImageView) findViewById(R.id.iv_title_bar_left);
        mCenterTitleTextView = (TextView) findViewById(R.id.tv_title_bar_title);
    }

    protected int getHeaderViewLayoutId() {
        return R.layout.cube_mints_base_header_bar_title;
    }

    public ImageView getLeftImageView() {
        return mLeftReturnImageView;
    }

    public TextView getTitleTextView() {
        return mCenterTitleTextView;
    }

    public void setTitle(String title) {
        mTitle = title;
        mCenterTitleTextView.setText(title);
    }

    public String getTitle() {
        return mTitle;
    }

    private RelativeLayout.LayoutParams makeLayoutParams(View view) {
        ViewGroup.LayoutParams lpOld = view.getLayoutParams();
        RelativeLayout.LayoutParams lp = null;
        if (lpOld == null) {
            lp = new RelativeLayout.LayoutParams(-2, -1);
        } else {
            lp = new RelativeLayout.LayoutParams(lpOld.width, lpOld.height);
        }
        return lp;
    }

    /**
     * set customized view to left side
     *
     * @param view the view to be added to left side
     */
    public void setCustomizedLeftView(View view) {
        mLeftReturnImageView.setVisibility(GONE);
        RelativeLayout.LayoutParams lp = makeLayoutParams(view);
        lp.addRule(CENTER_VERTICAL);
        lp.addRule(ALIGN_PARENT_LEFT);
        getLeftViewContainer().addView(view, lp);
    }

    /**
     * set customized view to left side
     *
     * @param layoutId the xml layout file id
     */
    public void setCustomizedLeftView(int layoutId) {
        View view = inflate(getContext(), layoutId, null);
        setCustomizedLeftView(view);
    }

    /**
     * set customized view to center
     *
     * @param view the view to be added to center
     */
    public void setCustomizedCenterView(View view) {
        mCenterTitleTextView.setVisibility(GONE);
        RelativeLayout.LayoutParams lp = makeLayoutParams(view);
        lp.addRule(CENTER_IN_PARENT);
        getCenterViewContainer().addView(view, lp);
    }

    /**
     * set customized view to center
     *
     * @param layoutId the xml layout file id
     */
    public void setCustomizedCenterView(int layoutId) {
        View view = inflate(getContext(), layoutId, null);
        setCustomizedCenterView(view);
    }

    /**
     * set customized view to right side
     *
     * @param view the view to be added to right side
     */
    public void setCustomizedRightView(View view) {
        RelativeLayout.LayoutParams lp = makeLayoutParams(view);
        lp.addRule(CENTER_VERTICAL);
        lp.addRule(ALIGN_PARENT_RIGHT);
        getRightViewContainer().addView(view, lp);
    }

    public RelativeLayout getLeftViewContainer() {
        return mLeftViewContainer;
    }

    public RelativeLayout getCenterViewContainer() {
        return mCenterViewContainer;
    }

    public RelativeLayout getRightViewContainer() {
        return mRightViewContainer;
    }

    public void setLeftOnClickListener(OnClickListener l) {
        mLeftViewContainer.setOnClickListener(l);
    }

    public void setCenterOnClickListener(OnClickListener l) {
        mCenterViewContainer.setOnClickListener(l);
    }

    public void setRightOnClickListener(OnClickListener l) {
        mRightViewContainer.setOnClickListener(l);
    }
}


package in.srain.cube.image.impl;

import android.os.Build;
import in.srain.cube.image.ImageTask;
import in.srain.cube.image.iface.ImageDownloader;
import in.srain.cube.image.iface.ProgressUpdateHandler;
import in.srain.cube.util.CLog;
import in.srain.cube.util.CubeDebug;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

/**
 * A simple class that fetches images from a URL.
 */
public class SimpleDownloader implements ImageDownloader {

    protected static final String LOG_TAG = CubeDebug.DEBUG_IMAGE_LOG_TAG_PROVIDER;

    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static SimpleDownloader sInstance;

    public static SimpleDownloader getInstance() {
        if (sInstance == null) {
            sInstance = new SimpleDownloader();
        }
        return sInstance;
    }

    /**
     * Download a bitmap from a URL and write the content to an output stream.
     */
    public boolean downloadToStream(ImageTask imageTask, String urlString, OutputStream outputStream, ProgressUpdateHandler progressUpdateHandler) {
        disableConnectionReuseIfNecessary();
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;

        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setReadTimeout(0);
            int len = urlConnection.getContentLength();
            int total = 0;
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
            int b;
            while ((b = in.read()) != -1) {
                total++;
                out.write(b);
                if (progressUpdateHandler != null) {
                    progressUpdateHandler.onProgressUpdate(total, len);
                }
            }
            return true;
        } catch (final IOException e) {
            CLog.e(LOG_TAG, "Error in downloadBitmap - " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {
            }
        }
        return false;
    }

    /**
     * Workaround for bug pre-Froyo, see here for more info: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
     */
    public static void disableConnectionReuseIfNecessary() {
        // HTTP connection reuse which was buggy pre-froyo
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) {
            System.setProperty("http.keepAlive", "false");
        }
    }
}


package in.srain.cube.image;

import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import in.srain.cube.image.iface.ImageLoadHandler;
import in.srain.cube.util.CLog;
import in.srain.cube.util.CubeDebug;
import in.srain.cube.util.Encrypt;

import java.lang.ref.WeakReference;

/**
 * A wrapper of the related information used in loading a bitmap
 *
 * @author http://www.liaohuqiu.net
 */
public class ImageTask {

    protected static final String LOG_TAG = CubeDebug.DEBUG_IMAGE_LOG_TAG_TASK;
    private static final Object sPoolSync = new Object();
    private static ImageTask sTop;
    private static int sPoolSize = 0;
    private static final int MAX_POOL_SIZE = 20;
    private static boolean USE_POOL = false;

    private static int sId = 0;

    private final static String SIZE_SP = "_";

    // 0000 0111
    private static final int ERROR_CODE_MASK = 0x07;

    // error code, max 0x07
    public final static int ERROR_NETWORK = 0x01;
    public final static int ERROR_BAD_FORMAT = 0x02;
    public final static int ERROR_EMPTY_URL = 0x03;

    /**
     * bits:
     * 1 error-code
     * 2 error-code
     * 3 error-code
     * 4 loading
     * 5 pre-load
     */
    private final static int STATUS_LOADING = 0x01 << 3;
    private final static int STATUS_PRE_LOAD = 0x02 << 3;

    private int mFlag = 0;
    protected int mId = 0;

    // the origin request url for the image.
    protected String mOriginUrl;

    // In some situations, we may store the same image in some different servers. So the same image will related to some different urls.
    private String mIdentityUrl;

    // The key related to the image this ImageTask is requesting.
    private String mIdentityKey;

    // cache for toString();
    private String mStr;

    protected Point mRequestSize = new Point();
    protected Point mBitmapOriginSize = new Point();
    protected ImageLoadRequest mRequest;

    protected ImageViewHolder mFirstImageViewHolder;
    protected ImageTaskStatistics mImageTaskStatistics;

    ImageTask next;

    private boolean mHasRecycled = false;

    protected void clearForRecycle() {
        mHasRecycled = true;
        mFlag = 0;

        mOriginUrl = null;
        mIdentityUrl = null;
        mIdentityKey = null;
        mStr = null;
        mRequestSize.set(0, 0);
        mBitmapOriginSize.set(0, 0);

        mRequest = null;
        mFirstImageViewHolder = null;
        mImageTaskStatistics = null;
    }

    public static ImageTask obtain() {
        if (!USE_POOL) {
            return null;
        }
        // pop top, make top.next as top
        synchronized (sPoolSync) {
            if (sTop != null) {
                ImageTask m = sTop;
                sTop = m.next;
                m.next = null;
                sPoolSize--;
                m.mHasRecycled = false;
                if (CubeDebug.DEBUG_IMAGE) {
                    CLog.d(LOG_TAG, "%s, obtain reused, pool remain: %d", m, sPoolSize);
                }
                return m;
            }
        }
        return null;
    }

    public void tryToRecycle() {
        if (!USE_POOL) {
            return;
        }
        clearForRecycle();

        // mark top as the next of current, then push current as pop
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sTop;
                sTop = this;
                sPoolSize++;
                if (CubeDebug.DEBUG_IMAGE) {
                    CLog.d(LOG_TAG, "%s is put to recycle poll, pool size: %d", this, sPoolSize);
                } else {
                    if (CubeDebug.DEBUG_IMAGE) {
                        CLog.d(LOG_TAG, "%s is not recycled, the poll is full: %d", this, sPoolSize);
                    }
                }
            }
        }
    }

    public ImageTask renewForRequest(ImageLoadRequest request) {
        if (CubeDebug.DEBUG_IMAGE) {
            int lastId = mId;
            mId = ++sId;
            CLog.d(LOG_TAG, "%s, renew: %s => %s", this, lastId, mId);
        } else {
            mId = ++sId;
        }
        mStr = null;
        if (ImagePerformanceStatistics.sample(mId)) {
            mImageTaskStatistics = new ImageTaskStatistics();
        }
        mOriginUrl = request.getUrl();
        mRequestSize.set(request.getRequestWidth(), request.getRequestHeight());
        mRequest = request;
        return this;
    }

    public ImageTask setOriginUrl(String originUrl) {
        mOriginUrl = originUrl;
        return this;
    }

    public ImageTask setRequestSize(int requestWidth, int requestHeight) {
        mRequestSize.set(requestWidth, requestHeight);
        return this;
    }

    /**
     * For accessing the identity url
     *
     * @return
     */
    public String getIdentityUrl() {
        if (null == mIdentityUrl) {
            mIdentityUrl = generateIdentityUrl(mOriginUrl);
        }
        return mIdentityUrl;
    }

    /**
     * In some situations, we may store the same image in some different servers. So the same image will related to some different urls.
     * Generate the identity url according your situation.
     * {@link #mIdentityUrl}
     *
     * @return
     * @deprecated Do not overwrite this method, use {@link in.srain.cube.image.iface.NameGenerator} instead.
     * should be used.
     */
    @Deprecated
    protected String generateIdentityUrl(String originUrl) {
        return ImageLoaderFactory.getNameGenerator().generateIdentityUrlFor(mRequest);
    }

    /**
     * Generate the identity key.
     *

     * This key should be related to the unique image this task is requesting: the size, the remote url.
     *
     * @return
     */
    protected String generateIdentityKey() {
        if (mRequest.getImageReuseInfo() == null) {
            return getIdentityUrl();
        } else {
            return joinSizeTagToKey(getIdentityUrl(), mRequest.getImageReuseInfo().getIdentitySize());
        }
    }

    public boolean isPreLoad() {
        return (mFlag & STATUS_PRE_LOAD) == STATUS_PRE_LOAD;
    }

    public void setIsPreLoad() {
        mFlag = mFlag | STATUS_PRE_LOAD;
    }

    public boolean isLoading() {
        return (mFlag & STATUS_LOADING) != 0;
    }

    /**
     * Check the given {@link in.srain.cube.image.ImageLoadRequest} is loading.
     *
     * @param request
     * @return Identify the given url, if same to {@link #mIdentityUrl} return true.
     */
    public boolean isLoadingThisUrl(ImageLoadRequest request) {
        String url2 = ImageLoaderFactory.getNameGenerator().generateIdentityUrlFor(request);
        return getIdentityUrl().equals(url2);
    }

    /**
     * Bind ImageView with ImageTask
     *
     * @param imageView
     */
    public void addImageView(CubeImageView imageView) {
        if (null == imageView) {
            return;
        }
        if (null == mFirstImageViewHolder) {
            mFirstImageViewHolder = new ImageViewHolder(imageView);
            return;
        }

        ImageViewHolder holder = mFirstImageViewHolder;
        for (; ; holder = holder.mNext) {
            if (holder.contains(imageView)) {
                return;
            }
            if (holder.mNext == null) {
                break;
            }
        }

        ImageViewHolder newHolder = new ImageViewHolder(imageView);
        newHolder.mPrev = holder;
        holder.mNext = newHolder;
    }

    /**
     * Remove the ImageView from ImageTask
     *
     * @param imageView
     */
    public void removeImageView(CubeImageView imageView) {

        if (null == imageView || null == mFirstImageViewHolder) {
            return;
        }

        ImageViewHolder holder = mFirstImageViewHolder;

        do {
            if (holder.contains(imageView)) {

                // Make sure entry is right.
                if (holder == mFirstImageViewHolder) {
                    mFirstImageViewHolder = holder.mNext;
                }
                if (null != holder.mNext) {
                    holder.mNext.mPrev = holder.mPrev;
                }
                if (null != holder.mPrev) {
                    holder.mPrev.mNext = holder.mNext;
                }
            }

        } while ((holder = holder.mNext) != null);
    }

    /**
     * Check if this ImageTask has any related ImageViews.
     *
     * @return
     */
    public boolean stillHasRelatedImageView() {
        if (null == mFirstImageViewHolder || mFirstImageViewHolder.getImageView() == null) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * When loading from network
     *
     * @param handler
     */
    public void onLoading(ImageLoadHandler handler) {
        mFlag = mFlag | STATUS_LOADING;

        if (null == handler) {
            return;
        }

        if (mFirstImageViewHolder == null) {
            handler.onLoading(this, null);
        } else {
            ImageViewHolder holder = mFirstImageViewHolder;
            do {
                final CubeImageView imageView = holder.getImageView();
                if (null != imageView) {
                    handler.onLoading(this, imageView);
                }
            } while ((holder = holder.mNext) != null);
        }
    }

    /**
     * notify loading
     *
     * @param handler
     * @param imageView
     */
    public void notifyLoading(ImageLoadHandler handler, CubeImageView imageView) {
        if (handler == null || imageView == null) {
            return;
        }
        handler.onLoading(this, imageView);
    }

    /**
     * Will be called when begin load image data from dish or network
     *
     * @param drawable
     */
    public void onLoadTaskFinish(BitmapDrawable drawable, ImageLoadHandler handler) {

        mFlag &= ~STATUS_LOADING;

        if (null == handler) {
            return;
        }
        int errorCode = mFlag & ERROR_CODE_MASK;
        if (errorCode > 0) {
            onLoadError(errorCode, handler);
            return;
        }

        if (null != mImageTaskStatistics) {
            mImageTaskStatistics.s5_beforeShow();
        }
        if (mFirstImageViewHolder == null) {
            handler.onLoadFinish(this, null, drawable);
        } else {
            ImageViewHolder holder = mFirstImageViewHolder;
            do {
                final CubeImageView imageView = holder.getImageView();
                if (null != imageView) {
                    imageView.onLoadFinish();
                    handler.onLoadFinish(this, imageView, drawable);
                }
            } while ((holder = holder.mNext) != null);
        }
        if (null != mImageTaskStatistics) {
            mImageTaskStatistics.s6_afterShow(ImageProvider.getBitmapSize(drawable));
            ImagePerformanceStatistics.onImageLoaded(this, mImageTaskStatistics);
        }
    }

    public void onLoadTaskCancel() {
    }

    public void setError(int errorCode) {
        if (errorCode > ERROR_CODE_MASK) {
            throw new IllegalArgumentException("error code undefined.");
        }

        // clear old error flag
        mFlag = (mFlag & ~ERROR_CODE_MASK);

        // set current error flag
        mFlag |= errorCode;
    }

    private void onLoadError(int reason, ImageLoadHandler handler) {
        if (mFirstImageViewHolder == null) {
            handler.onLoadError(this, null, reason);
        } else {
            ImageViewHolder holder = mFirstImageViewHolder;
            do {
                final CubeImageView imageView = holder.getImageView();
                if (null != imageView) {
                    imageView.onLoadFinish();
                    handler.onLoadError(this, imageView, reason);
                }
            } while ((holder = holder.mNext) != null);
        }
    }

    /**
     * If you have a thumbnail web service which can return multiple size image according the url,
     *

     * you can implements this method to return the specified url according the request size.
     *
     * @return
     */
    public String getRemoteUrl() {
        return mOriginUrl;
    }

    /**
     * Return the origin request url
     *
     * @return
     */
    public String getOriginUrl() {
        return mOriginUrl;
    }

    public void setBitmapOriginSize(int width, int height) {
        mBitmapOriginSize.set(width, height);
    }

    public Point getBitmapOriginSize() {
        return mBitmapOriginSize;
    }

    public Point getRequestSize() {
        return mRequestSize;
    }

    /**
     * Return the key which identifies this Image Wrapper object.
     */
    public String getIdentityKey() {
        if (mIdentityKey == null) {
            mIdentityKey = generateIdentityKey();
        }
        return mIdentityKey;
    }

    /**
     * Join the key and the size information.
     *
     * @param key
     * @param w
     * @param h
     * @return "$key" + "_" + "$w" + "_" + "$h"
     */
    public static String joinSizeInfoToKey(String key, int w, int h) {
        if (w > 0 && h != Integer.MAX_VALUE && h > 0 && h != Integer.MAX_VALUE) {
            return new StringBuilder(key).append(SIZE_SP).append(w).append(SIZE_SP).append(h).toString();
        }
        return key;
    }

    /**
     * Join the tag with the key.
     *
     * @param key
     * @param tag
     * @return "$key" + "_" + "$tag"
     */
    public static String joinSizeTagToKey(String key, String tag) {
        return new StringBuilder(key).append(SIZE_SP).append(tag).toString();
    }

    /**
     * Return the cache key for file cache.
     *
     * @return the cache key for file cache.
     */
    public String getFileCacheKey() {
        return Encrypt.md5(getIdentityKey());
    }

    /**
     * @param sizeKey
     * @return
     */
    public String generateFileCacheKeyForReuse(String sizeKey) {
        return Encrypt.md5(joinSizeTagToKey(getIdentityUrl(), sizeKey));
    }

    @Override
    public boolean equals(Object object) {
        if (object != null && object instanceof ImageTask) {
            return ((ImageTask) object).getIdentityKey().equals(getIdentityKey());
        }
        return false;
    }

    @Override
    public String toString() {
        if (mStr == null) {
            mStr = String.format("[ImageTask@%s %s %sx%s %s]", Integer.toHexString(hashCode()), mId, mRequestSize.x, mRequestSize.y, mHasRecycled);
        }
        return mStr;
    }

    public ImageTaskStatistics getStatistics() {
        return mImageTaskStatistics;
    }

    /**
     * A tiny and light linked-list like container to hold all the ImageViews related to the ImageTask.
     */
    private static class ImageViewHolder {
        private WeakReference<CubeImageView> mImageViewRef;
        private ImageViewHolder mNext;
        private ImageViewHolder mPrev;

        public ImageViewHolder(CubeImageView imageView) {
            mImageViewRef = new WeakReference<CubeImageView>(imageView);
        }

        boolean contains(CubeImageView imageView) {
            return mImageViewRef != null && imageView == mImageViewRef.get();
        }

        CubeImageView getImageView() {
            if (null == mImageViewRef) {
                return null;
            }
            return mImageViewRef.get();
        }
    }

    public ImageLoadRequest getRequest() {
        return mRequest;
    }
}


package in.srain.cube.cache;

import android.content.Context;
import in.srain.cube.concurrent.SimpleExecutor;
import in.srain.cube.concurrent.SimpleTask;
import in.srain.cube.diskcache.CacheEntry;
import in.srain.cube.diskcache.DiskCache;
import in.srain.cube.diskcache.lru.SimpleDiskLruCache;
import in.srain.cube.util.CLog;

import java.io.File;
import java.io.IOException;

/**
 * It's easy to use.
 * Auto open.
 */
public class DiskCacheProvider {

    public interface AsyncTaskEventHandler {
        public void onEvent(int type);
    }

    public static final boolean DEBUG = true;
    protected static final String LOG_TAG = "cube-disk-cache-provider";

    public static final byte TASK_INIT_CACHE = 1;
    public static final byte TASK_CLOSE_CACHE = 2;
    public static final byte TASK_FLUSH_CACHE = 3;

    protected DiskCache mDiskCache;
    private boolean mIsDelayFlushing = false;
    private final Object mDiskCacheLock = new Object();
    private boolean mDiskCacheStarting = true;
    private boolean mDiskCacheReady = false;

    private AsyncTaskEventHandler mAsyncTaskEventHandler;

    public void setAsyncTaskEventHandler(AsyncTaskEventHandler handler) {
        mAsyncTaskEventHandler = handler;
    }

    public DiskCacheProvider(DiskCache diskCache) {
        mDiskCache = diskCache;
    }

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

    public void write(String key, String str) {
        if (key == null) {
            return;
        }
        try {
            CacheEntry cacheEntry = getDiskCache().beginEdit(key);
            cacheEntry.setString(str);
            cacheEntry.commit();
        } catch (Exception e) {
            if (DEBUG) {
                e.printStackTrace();
            }
        }
    }

    public String read(String fileCacheKey) {
        try {
            CacheEntry cacheEntry = getDiskCache().getEntry(fileCacheKey);
            if (cacheEntry != null) {
                return cacheEntry.getString();
            }
        } catch (Exception e) {
            if (DEBUG) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * initiate the disk cache
     */
    public void openDiskCacheAsync() {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s: initDiskCacheAsync", mDiskCache);
        }
        synchronized (mDiskCacheLock) {
            mDiskCacheStarting = true;
            new FileCacheTask(TASK_INIT_CACHE).executeNow();
        }
    }

    /**
     * close the disk cache
     */
    public void closeDiskCacheAsync() {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s: closeDiskCacheAsync", mDiskCache);
        }
        new FileCacheTask(TASK_CLOSE_CACHE).executeNow();
    }

    /**
     * flush the data to disk cache
     */
    public void flushDiskCacheAsync() {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s, flushDishCacheAsync", mDiskCache);
        }
        new FileCacheTask(TASK_FLUSH_CACHE).executeNow();
    }

    /**
     * flush the data to disk cache
     */
    public void flushDiskCacheAsyncWithDelay(int delay) {
        if (DEBUG) {
            CLog.d(LOG_TAG, "%s, flushDiskCacheAsyncWithDelay", delay);
        }
        if (mIsDelayFlushing) {
            return;
        }
        mIsDelayFlushing = true;
        new FileCacheTask(TASK_FLUSH_CACHE).executeAfter(delay);
    }

    /**
     * If disk is not read, will prepare it first.
     *
     * @return
     */
    public DiskCache getDiskCache() {
        if (!mDiskCacheReady) {
            if (DEBUG) {
                CLog.d(LOG_TAG, "%s, try to access disk cache, but it is not open, try to open it.", mDiskCache);
            }
            openDiskCacheAsync();
        }
        synchronized (mDiskCacheLock) {
            while (mDiskCacheStarting) {
                try {
                    if (DEBUG) {
                        CLog.d(LOG_TAG, "%s, try to access, but disk cache is not ready, wait", mDiskCache);
                    }
                    mDiskCacheLock.wait();
                } catch (InterruptedException e) {
                }
            }
        }
        return mDiskCache;
    }

    /**
     * A helper class to encapsulate the operate into a Work which will be executed by the Worker.
     */
    private class FileCacheTask extends SimpleTask {

        private byte mTaskType;

        private FileCacheTask(byte taskType) {
            mTaskType = taskType;
        }

        @Override
        public void doInBackground() {

            try {
                doWork();
            } catch (IOException e) {
                if (DEBUG) {
                    e.printStackTrace();
                }
            }
        }

        private void doWork() throws IOException {
            switch (mTaskType) {
                case TASK_INIT_CACHE:

                    synchronized (mDiskCacheLock) {
                        if (DEBUG) {
                            CLog.d(LOG_TAG, "begin open disk cache: " + mDiskCache);
                        }
                        mDiskCache.open();
                        mDiskCacheReady = true;
                        mDiskCacheStarting = false;
                        if (DEBUG) {
                            CLog.d(LOG_TAG, "disk cache open successfully, notify all lock: " + mDiskCache);
                        }
                        mDiskCacheLock.notifyAll();
                    }

                    break;
                case TASK_CLOSE_CACHE:
                    mDiskCache.close();
                    break;
                case TASK_FLUSH_CACHE:
                    mDiskCache.flush();
                    break;
                default:
                    break;
            }
        }

        @Override
        public void onFinish(boolean canceled) {
            if (mAsyncTaskEventHandler != null) {
                mAsyncTaskEventHandler.onEvent(mTaskType);
            }
        }

        void executeNow() {
            SimpleExecutor.getInstance().execute(this);
        }

        void executeAfter(int delay) {
            postDelay(new Runnable() {
                @Override
                public void run() {
                    executeNow();
                }
            }, delay);
        }
    }
}


package in.srain.cube.diskcache;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileUtils {

    /**
     * Returns the remainder of 'reader' as a string, closing it when done.
     */
    public static String readFully(Reader reader) throws IOException {
        try {
            StringWriter writer = new StringWriter();
            char[] buffer = new char[1024];
            int count;
            while ((count = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, count);
            }
            return writer.toString();
        } finally {
            reader.close();
        }
    }

    /**
     * Returns the ASCII characters up to but not including the next "\r\n", or
     * "\n".
     *
     * @throws java.io.EOFException if the stream is exhausted before the next newline
     *                              character.
     */
    public static String readAsciiLine(InputStream in) throws IOException {
        StringBuilder result = new StringBuilder(80);
        while (true) {
            int c = in.read();
            if (c == -1) {
                throw new EOFException();
            } else if (c == '\n') {
                break;
            }

            result.append((char) c);
        }
        int length = result.length();
        if (length > 0 && result.charAt(length - 1) == '\r') {
            result.setLength(length - 1);
        }
        return result.toString();
    }

    /**
     * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
     */
    public static void closeQuietly(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (RuntimeException rethrown) {
                throw rethrown;
            } catch (Exception ignored) {
            }
        }
    }

    /**
     * Try to delete directory in a fast way.
     */
    public static void deleteDirectoryQuickly(File dir) throws IOException {

        if (!dir.exists()) {
            return;
        }
        final File to = new File(dir.getAbsolutePath() + System.currentTimeMillis());
        dir.renameTo(to);
        if (!dir.exists()) {
            // rebuild
            dir.mkdirs();
        }

        // try to run "rm -r" to remove the whole directory
        if (to.exists()) {
            String deleteCmd = "rm -r " + to;
            Runtime runtime = Runtime.getRuntime();
            try {
                Process process = runtime.exec(deleteCmd);
                process.waitFor();
            } catch (IOException e) {

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (!to.exists()) {
            return;
        }
        deleteDirectoryRecursively(to);
        if (to.exists()) {
            to.delete();
        }
    }

    public static void chmod(String mode, String path) {
        try {
            String command = "chmod " + mode + " " + path;
            Runtime runtime = Runtime.getRuntime();
            Process process = runtime.exec(command);
            process.waitFor();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * recursively delete
     *
     * @param dir
     * @throws java.io.IOException
     */
    public static void deleteDirectoryRecursively(File dir) throws IOException {
        File[] files = dir.listFiles();
        if (files == null) {
            throw new IllegalArgumentException("not a directory: " + dir);
        }
        for (File file : files) {
            if (file.isDirectory()) {
                deleteDirectoryRecursively(file);
            }
            if (!file.delete()) {
                throw new IOException("failed to delete file: " + file);
            }
        }
    }

    public static void deleteIfExists(File file) throws IOException {
        if (file.exists() && !file.delete()) {
            throw new IOException();
        }
    }

    public static boolean writeString(String filePath, String content) {
        File file = new File(filePath);
        if (!file.getParentFile().exists())
            file.getParentFile().mkdirs();

        FileWriter writer = null;
        try {

            writer = new FileWriter(file);
            writer.write(content);

        } catch (IOException e) {
        } finally {
            try {
                if (writer != null) {

                    writer.close();
                    return true;
                }
            } catch (IOException e) {
            }
        }
        return false;
    }

    public static String readString(String filePath) {
        File file = new File(filePath);
        if (!file.exists())
            return null;

        FileInputStream fileInput = null;
        FileChannel channel = null;
        try {
            fileInput = new FileInputStream(filePath);
            channel = fileInput.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());
            channel.read(buffer);

            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.write(buffer.array());
            return byteArrayOutputStream.toString();
        } catch (Exception e) {
        } finally {

            if (fileInput != null) {
                try {
                    fileInput.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (channel != null) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}


package in.srain.cube.cache;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.AssetManager;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.text.TextUtils;
import in.srain.cube.util.CLog;
import in.srain.cube.util.Version;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.InputStream;

public class DiskFileUtils {

    /**
     * @param context
     * @param absolutePath         if it's not absolutePath, will be path under cache dir
     * @param sizeInKB
     * @param fallbackRelativePath
     * @return
     */
    public static CacheDirInfo getDiskCacheDir(
            Context context, String absolutePath, int sizeInKB,
            String fallbackRelativePath) {

        long size = (long) sizeInKB * 1024;
        boolean done = false;

        CacheDirInfo dirInfo = new CacheDirInfo();
        dirInfo.requireSize = size;

        // treat as absolute path
        if (!TextUtils.isEmpty(absolutePath)) {
            File cachePath = new File(absolutePath);
            // is not exist, try to make parent directory
            if (cachePath.exists() || cachePath.mkdirs()) {
                long free = getUsableSpace(cachePath);
                size = Math.min(size, free);
                dirInfo.realSize = size;
                dirInfo.path = cachePath;
                done = true;
            }
        }

        // it's relative path
        if (!done) {
            if (TextUtils.isEmpty(fallbackRelativePath)) {
                fallbackRelativePath = absolutePath;
            }
            dirInfo = getDiskCacheDir(context, fallbackRelativePath, size);
        }
        return dirInfo;
    }

    /**
     * Get a usable cache directory (external if available, internal otherwise).
     * .
     * Check if media is mounted or storage is built-in, if so, try and use external cache folder
     * otherwise use internal cache folder
     * .
     * If both of them can not meet the requirement, use the bigger one.
     *
     * @param context    The context to use
     * @param uniqueName A unique folder name to append to the cache folder
     * @return The cache folder
     */
    public static CacheDirInfo getDiskCacheDir(Context context, String uniqueName, long requireSpace) {
        File sdPath = null;
        File internalPath = null;
        Long sdCardFree = 0L;

        boolean usingInternal = false;

        if (hasSDCardMounted()) {
            sdPath = getExternalCacheDir(context);
            if (!sdPath.exists()) {
                sdPath.mkdirs();
            }
            sdCardFree = getUsableSpace(sdPath);
        }

        CacheDirInfo cacheDirInfo = new CacheDirInfo();
        cacheDirInfo.requireSize = requireSpace;

        // sd card can not meet the requirement
        // try to use the build-in storage
        if (sdPath == null || sdCardFree < requireSpace) {
            internalPath = context.getCacheDir();
            long internalFree = getUsableSpace(internalPath);

            // both lower then requirement, choose the bigger one
            if (internalFree < requireSpace) {
                if (internalFree > sdCardFree) {
                    usingInternal = true;
                    cacheDirInfo.realSize = internalFree;
                } else {
                    usingInternal = false;
                    cacheDirInfo.realSize = sdCardFree;
                }
                cacheDirInfo.isNotEnough = true;
            } else {
                usingInternal = true;
                cacheDirInfo.realSize = requireSpace;
            }
        } else {
            usingInternal = false;
            cacheDirInfo.realSize = requireSpace;
        }

        cacheDirInfo.isInternal = usingInternal;
        if (usingInternal) {
            cacheDirInfo.path = new File(internalPath.getPath() + File.separator + uniqueName);
        } else {
            cacheDirInfo.path = new File(sdPath.getPath() + File.separator + uniqueName);
        }
        if (!cacheDirInfo.path.exists() && !cacheDirInfo.path.mkdirs()) {
            CLog.e("cube-cache", "can not create directory for: %s", cacheDirInfo.path);
        }
        return cacheDirInfo;
    }

    /**
     * Get the external application cache directory.
     *
     * @param context The context to use
     * @return The external cache folder : /storage/sdcard0/Android/data/com.srain.sdk/cache
     */
    @TargetApi(Build.VERSION_CODES.FROYO)
    public static File getExternalCacheDir(Context context) {
        if (Version.hasFroyo()) {
            File path = context.getExternalCacheDir();

            // In some case, even the sd card is mounted, getExternalCacheDir will return null, may be it is nearly full.
            if (path != null) {
                return path;
            }
        }

        // Before Froyo or the path is null, we need to construct the external cache folder ourselves
        final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
        return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
    }

    /**
     * Check how much usable space is available at a given path.
     *
     * @param path The path to check
     * @return The space available in bytes by user, not by root, -1 means path is null, 0 means path is not exist.
     */
    @SuppressWarnings("deprecation")
    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    public static long getUsableSpace(File path) {
        if (path == null) {
            return -1;
        }
        if (Version.hasGingerbread()) {
            return path.getUsableSpace();
        } else {
            if (!path.exists()) {
                return 0;
            } else {
                final StatFs stats = new StatFs(path.getPath());
                return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
            }
        }
    }

    @SuppressWarnings("deprecation")
    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    public static long getUsedSpace(File path) {
        if (path == null) {
            return -1;
        }

        if (Version.hasGingerbread()) {
            return path.getTotalSpace() - path.getUsableSpace();
        } else {
            if (!path.exists()) {
                return -1;
            } else {
                final StatFs stats = new StatFs(path.getPath());
                return (long) stats.getBlockSize() * (stats.getBlockCount() - stats.getAvailableBlocks());
            }
        }
    }

    /**
     * @param path
     * @return -1 means path is null, 0 means path is not exist.
     */
    @SuppressWarnings("deprecation")
    @TargetApi(Build.VERSION_CODES.GINGERBREAD)
    public static long getTotalSpace(File path) {
        if (path == null) {
            return -1;
        }
        if (Version.hasGingerbread()) {
            return path.getTotalSpace();
        } else {
            if (!path.exists()) {
                return 0;
            } else {
                final StatFs stats = new StatFs(path.getPath());
                return (long) stats.getBlockSize() * (long) stats.getBlockCount();
            }
        }
    }

    public static boolean hasSDCardMounted() {
        String state = Environment.getExternalStorageState();
        if (state != null && state.equals(Environment.MEDIA_MOUNTED)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * external: "/storage/emulated/0/Android/data/in.srain.sample/files"
     * internal: "/data/data/in.srain.sample/files"
     */
    public static String wantFilesPath(Context context, boolean externalStorageFirst) {
        String path = null;
        File f = null;
        if (externalStorageFirst && DiskFileUtils.hasSDCardMounted() && (f = context.getExternalFilesDir("xxx")) != null) {
            path = f.getAbsolutePath();
        } else {
            path = context.getFilesDir().getAbsolutePath();
        }
        return path;
    }

    /**
     * @param context
     * @param filePath file path relative to assets, like request_init1/search_index.json
     * @return
     */
    public static String readAssert(Context context, String filePath) {
        try {
            if (filePath.startsWith(File.separator)) {
                filePath = filePath.substring(File.separator.length());
            }
            AssetManager assetManager = context.getAssets();
            InputStream inputStream = assetManager.open(filePath);
            DataInputStream stream = new DataInputStream(inputStream);
            int length = stream.available();
            byte[] buffer = new byte[length];
            stream.readFully(buffer);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byteArrayOutputStream.write(buffer);
            stream.close();
            return byteArrayOutputStream.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static class CacheDirInfo {
        public File path;
        public boolean isInternal = false;
        public boolean isNotEnough = false;
        public long realSize;
        public long requireSize;
    }
}


package in.srain.cube.image;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import in.srain.cube.image.drawable.RecyclingBitmapDrawable;

/**
 * Sub-class of ImageView which:
 *

 *

 * automatically notifies the Drawable when it is being displayed.
 *

 *

 * Part of the code is taken from the Android best practice of displaying Bitmaps
 *

 * Displaying Bitmaps Efficiently.
 */
public class CubeImageView extends ImageView {

    private ImageLoader mImageLoader;

    private ImageTask mImageTask;
    private boolean mClearWhenDetached = true;
    private String mStr;
    private ImageLoadRequest mRequest;

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

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

    public CubeImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    /**
     * Notifies the drawable that it's displayed state has changed.
     *
     * @param drawable
     * @param isDisplayed
     */
    private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
        if (drawable instanceof RecyclingBitmapDrawable) {
            // The drawable is a CountingBitmapDrawable, so notify it
            ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
        } else if (drawable instanceof LayerDrawable) {
            // The drawable is a LayerDrawable, so recurse on each layer
            LayerDrawable layerDrawable = (LayerDrawable) drawable;
            for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
                notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
            }
        }
    }

    public void setClearDrawableWhenDetached(boolean clearWhenDetached) {
        mClearWhenDetached = clearWhenDetached;
    }

    public void clearDrawable() {
        setImageDrawable(null);

        if (null != mImageTask && null != mImageLoader) {
            mImageLoader.detachImageViewFromImageTask(mImageTask, this);
            clearLoadTask();
        }
    }

    /**
     * @see android.widget.ImageView#onDetachedFromWindow()
     */
    @Override
    protected void onDetachedFromWindow() {
        // This has been detached from Window, so clear the drawable
        // If this view is recycled ???
        if (mClearWhenDetached) {
            clearDrawable();
        }
        super.onDetachedFromWindow();
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        if (mClearWhenDetached) {
            tryLoadImage();
        }
    }

    /**
     * @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)
     */
    @Override
    public void setImageDrawable(Drawable drawable) {

        // Keep hold of previous Drawable
        final Drawable previousDrawable = getDrawable();

        super.setImageDrawable(drawable);

        // Notify new Drawable that it is being displayed
        notifyDrawable(drawable, true);

        // Notify old Drawable so it is no longer being displayed
        notifyDrawable(previousDrawable, false);
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        clearLoadTask();
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        clearLoadTask();
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        clearLoadTask();
    }

    public void onLoadFinish() {
    }

    private void clearLoadTask() {
        mImageTask = null;
    }

    public void loadImage(ImageLoader imageLoader, String url) {
        loadImage(imageLoader, url, 0, 0, null);
    }

    public void loadImage(ImageLoader imageLoader, String url, ImageReuseInfo imageReuseInfo) {
        loadImage(imageLoader, url, 0, 0, imageReuseInfo);
    }

    public void loadImage(ImageLoader imageLoader, String url, int specifiedSize) {
        loadImage(imageLoader, url, specifiedSize, specifiedSize, null);
    }

    public void loadImage(ImageLoader imageLoader, String url, int specifiedSize, ImageReuseInfo imageReuseInfo) {
        loadImage(imageLoader, url, specifiedSize, specifiedSize, imageReuseInfo);
    }

    public void loadImage(ImageLoader imageLoader, String url, int specifiedWidth, int specifiedHeight) {
        loadImage(imageLoader, url, specifiedWidth, specifiedHeight, null);
    }

    public void loadImage(ImageLoader imageLoader, ImageLoadRequest request) {
        mRequest = request;
        mImageLoader = imageLoader;

        if (request == null || TextUtils.isEmpty(request.getUrl())) {
            mImageLoader.getImageLoadHandler().onLoadError(mImageTask, this, ImageTask.ERROR_EMPTY_URL);
            mImageTask = null;
            return;
        }
        tryLoadImage();
    }

    public void loadImage(ImageLoader imageLoader, String url, int specifiedWidth, int specifiedHeight, ImageReuseInfo imageReuseInfo) {
        mRequest = new ImageLoadRequest(url, specifiedWidth, specifiedHeight, 0, imageReuseInfo);
        loadImage(imageLoader, mRequest);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        tryLoadImage();
    }

    private void tryLoadImage() {

        if (mRequest == null || TextUtils.isEmpty(mRequest.getUrl())) {
            return;
        }

        int width = getWidth();
        int height = getHeight();

        ViewGroup.LayoutParams lyp = getLayoutParams();
        boolean isFullyWrapContent = lyp != null && lyp.height == LayoutParams.WRAP_CONTENT && lyp.width == LayoutParams.WRAP_CONTENT;
        // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content
        // view, hold off on loading the image.
        if (width == 0 && height == 0 && !isFullyWrapContent) {
            return;
        }

        mRequest.setLayoutSize(width, height);

        // 1. Check the previous ImageTask related to this ImageView
        if (null != mImageTask) {

            // duplicated ImageTask, return directly.
            if (mImageTask.isLoadingThisUrl(mRequest)) {
                return;
            }
            // ImageView is reused, detach it from the related ImageViews of the previous ImageTask.
            else {
                mImageLoader.detachImageViewFromImageTask(mImageTask, this);
            }
        }

        // 2. Let the ImageView hold this ImageTask. When ImageView is reused next time, check it in step 1.
        ImageTask imageTask = mImageLoader.createImageTask(mRequest);
        //.createImageTask(mUrl, width, height, mImageReuseInfo);
        mImageTask = imageTask;

        // 3. Query cache, if hit, return at once.
        boolean hitCache = mImageLoader.queryCache(imageTask, this);
        if (hitCache) {
            return;
        } else {
            mImageLoader.addImageTask(mImageTask, this);
        }
    }

    @Override
    public String toString() {
        if (mStr == null) {
            mStr = "[CubeImageView@" + Integer.toHexString(hashCode()) + ']';
        }
        return mStr;
    }
}


package in.srain.cube.app;

import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import in.srain.cube.app.lifecycle.IComponentContainer;
import in.srain.cube.app.lifecycle.LifeCycleComponent;
import in.srain.cube.app.lifecycle.LifeCycleComponentManager;
import in.srain.cube.util.CLog;
import in.srain.cube.util.CubeDebug;

/**
 * Implement {@link ICubeFragment}, {@link IComponentContainer}
 * <p/>
 * Ignore {@link LifeCycleComponentManager#onBecomesPartiallyInvisible}
 */
public abstract class CubeFragment extends Fragment implements ICubeFragment, IComponentContainer {

    private static final boolean DEBUG = CubeDebug.DEBUG_LIFE_CYCLE;
    protected Object mDataIn;
    private boolean mFirstResume = true;

    private LifeCycleComponentManager mComponentContainer = new LifeCycleComponentManager();

    protected abstract View createView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);

    public CubeFragmentActivity getContext() {
        return (CubeFragmentActivity) getActivity();
    }

    /**
     * ===========================================================
     * Implements {@link ICubeFragment}
     * ===========================================================
     */
    @Override
    public void onEnter(Object data) {
        mDataIn = data;
        if (DEBUG) {
            showStatus("onEnter");
        }
    }

    @Override
    public void onLeave() {
        if (DEBUG) {
            showStatus("onLeave");
        }
        mComponentContainer.onBecomesTotallyInvisible();
    }

    @Override
    public void onBackWithData(Object data) {
        if (DEBUG) {
            showStatus("onBackWithData");
        }
        mComponentContainer.onBecomesVisibleFromTotallyInvisible();
    }

    @Override
    public boolean processBackPressed() {
        return false;
    }

    @Override
    public void onBack() {
        if (DEBUG) {
            showStatus("onBack");
        }
        mComponentContainer.onBecomesVisibleFromTotallyInvisible();
    }

    /**
     * ===========================================================
     * Implements {@link IComponentContainer}
     * ===========================================================
     */
    @Override
    public void addComponent(LifeCycleComponent component) {
        mComponentContainer.addComponent(component);
    }

    /**
     * Not add self to back stack when removed, so only when Activity stop
     */
    @Override
    public void onStop() {
        super.onStop();
        if (DEBUG) {
            showStatus("onStop");
        }
        onLeave();
    }

    /**
     * Only when Activity resume, not very precise.
     * When activity recover from partly invisible, onBecomesPartiallyInvisible will be triggered.
     */
    @Override
    public void onResume() {
        super.onResume();
        if (!mFirstResume) {
            onBack();
        }
        if (mFirstResume) {
            mFirstResume = false;
        }
        if (DEBUG) {
            showStatus("onResume");
        }
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        if (DEBUG) {
            showStatus("onAttach");
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (DEBUG) {
            showStatus("onCreate");
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (DEBUG) {
            showStatus("onActivityCreated");
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        if (DEBUG) {
            showStatus("onStart");
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (DEBUG) {
            showStatus("onPause");
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        if (DEBUG) {
            showStatus("onDestroyView");
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (DEBUG) {
            showStatus("onDestroy");
        }
        mComponentContainer.onDestroy();
    }

    @Override
    public void onDetach() {
        super.onDetach();
        if (DEBUG) {
            showStatus("onDetach");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (DEBUG) {
            showStatus("onCreateView");
        }
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    private void showStatus(String status) {
        final String[] className = ((Object) this).getClass().getName().split("\\.");
        CLog.d("cube-lifecycle", "%s %s", className[className.length - 1], status);
    }
}


package in.srain.cube.views.list;

import java.util.ArrayList;
import java.util.List;

public class ListPageInfo<T> {

    private int mNumPerPage = 0;
    private int mStart = 0;
    private int mLastStart = 0;
    private boolean mHasMore;
    private boolean mIsBusy = false;

    private List<T> mDataList;

    public ListPageInfo(int numPerPage) {
        mNumPerPage = numPerPage;
    }

    public void updateListInfo(List<T> dataList, int total) {
        addMore(dataList);
        mHasMore = mDataList.size() < total;
        mIsBusy = false;
    }

    private void addMore(List<T> dataList) {
        if (dataList == null) {
            return;
        }
        if (mStart == 0 || mDataList == null) {
            mDataList = new ArrayList<T>();
        }
        mDataList.addAll(dataList);
    }

    public void updateListInfo(List<T> dataList, boolean hasMore) {
        addMore(dataList);
        mHasMore = hasMore;
        mIsBusy = false;
    }

    public void rollbackOnFail() {
        mIsBusy = false;
        mStart = mLastStart;
    }

    public boolean tryEnterLock() {
        if (mIsBusy) {
            return false;
        }
        mIsBusy = true;
        return true;
    }

    public int getStart() {
        return mStart;
    }

    public int getNumPerPage() {
        return mNumPerPage;
    }

    /**
     * start from 0
     *
     * @return
     */
    public int getPage() {
        return mStart / mNumPerPage;
    }

    public void goToHead() {
        mStart = 0;
    }

    public T getItem(int position) {
        if (null == mDataList || position < 0 || position > mDataList.size()) {
            return null;
        }
        return mDataList.get(position);
    }

    public boolean isEmpty() {
        return mDataList == null || mDataList.size() == 0;
    }

    /**
     * try to move to next page
     *
     * @return
     */
    public boolean prepareForNextPage() {
        if (hasMore()) {
            mLastStart = mStart;
            mStart += mNumPerPage;
            return true;
        }
        return false;
    }

    public List<T> getDataList() {
        return mDataList;
    }

    public int getListLength() {
        if (mDataList == null) {
            return 0;
        }
        return mDataList.size();
    }

    /**
     * the first item in list
     *
     * @return
     */
    public T firstItem() {
        if (mDataList == null || mDataList.size() == 0) {
            return null;
        }
        return mDataList.get(0);
    }

    /**
     * the last item in list
     *
     * @return
     */
    public T lastItem() {
        if (mDataList == null || mDataList.size() == 0) {
            return null;
        }
        return mDataList.get(mDataList.size() - 1);
    }

    public boolean hasMore() {
        return mDataList == null || mHasMore;
    }

    public boolean isFirstPage() {
        return 0 == mStart;
    }
}


package com.gordonwong.materialsheetfab.sample;

import android.content.Context;
import android.support.design.widget.FloatingActionButton;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.ScaleAnimation;

import com.gordonwong.materialsheetfab.AnimatedFab;

/**
 * Created by Gordon Wong on 7/17/2015.
 *
 * Sample floating action button implementation.
 */
public class Fab extends FloatingActionButton implements AnimatedFab {

	private static final int FAB_ANIM_DURATION = 200;

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

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

	public Fab(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
	}

	/**
	 * Shows the FAB.
	 */
	@Override
	public void show() {
		show(0, 0);
	}

	/**
	 * Shows the FAB and sets the FAB's translation.
	 *
	 * @param translationX translation X value
	 * @param translationY translation Y value
	 */
	@Override
	public void show(float translationX, float translationY) {
		// Set FAB's translation
		setTranslation(translationX, translationY);

		// Only use scale animation if FAB is hidden
		if (getVisibility() != View.VISIBLE) {
			// Pivots indicate where the animation begins from
			float pivotX = getPivotX() + translationX;
			float pivotY = getPivotY() + translationY;

			ScaleAnimation anim;
			// If pivots are 0, that means the FAB hasn't been drawn yet so just use the
			// center of the FAB
			if (pivotX == 0 || pivotY == 0) {
				anim = new ScaleAnimation(0, 1, 0, 1, Animation.RELATIVE_TO_SELF, 0.5f,
						Animation.RELATIVE_TO_SELF, 0.5f);
			} else {
				anim = new ScaleAnimation(0, 1, 0, 1, pivotX, pivotY);
			}

			// Animate FAB expanding
			anim.setDuration(FAB_ANIM_DURATION);
			anim.setInterpolator(getInterpolator());
			startAnimation(anim);
		}
		setVisibility(View.VISIBLE);
	}

	/**
	 * Hides the FAB.
	 */
	@Override
	public void hide() {
		// Only use scale animation if FAB is visible
		if (getVisibility() == View.VISIBLE) {
			// Pivots indicate where the animation begins from
			float pivotX = getPivotX() + getTranslationX();
			float pivotY = getPivotY() + getTranslationY();

			// Animate FAB shrinking
			ScaleAnimation anim = new ScaleAnimation(1, 0, 1, 0, pivotX, pivotY);
			anim.setDuration(FAB_ANIM_DURATION);
			anim.setInterpolator(getInterpolator());
			startAnimation(anim);
		}
		setVisibility(View.INVISIBLE);
	}

	private void setTranslation(float translationX, float translationY) {
		animate().setInterpolator(getInterpolator()).setDuration(FAB_ANIM_DURATION)
				.translationX(translationX).translationY(translationY);
	}

	private Interpolator getInterpolator() {
		return AnimationUtils.loadInterpolator(getContext(), R.interpolator.msf_interpolator);
	}
}


package io.codetail.animation;

import android.annotation.TargetApi;
import android.os.Build;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ObjectAnimator;
import com.nineoldandroids.view.ViewHelper;
import com.nineoldandroids.view.ViewPropertyAnimator;

import java.lang.ref.WeakReference;

import io.codetail.animation.RevealAnimator.RevealInfo;

import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static io.codetail.animation.RevealAnimator.CLIP_RADIUS;

public class ViewAnimationUtils {

    private final static boolean LOLLIPOP_PLUS = SDK_INT >= LOLLIPOP;

    public static final int SCALE_UP_DURATION = 500;

    /**
     * Returns an Animator which can animate a clipping circle.
     *

     * Any shadow cast by the View will respect the circular clip from this animator.
     *

     * Only a single non-rectangular clip can be applied on a View at any time.
     * Views clipped by a circular reveal animation take priority over
     * {@link android.view.View#setClipToOutline(boolean) View Outline clipping}.
     *

     * Note that the animation returned here is a one-shot animation. It cannot
     * be re-used, and once started it cannot be paused or resumed.
     *
     * @param view The View will be clipped to the animating circle.
     * @param centerX The x coordinate of the center of the animating circle.
     * @param centerY The y coordinate of the center of the animating circle.
     * @param startRadius The starting radius of the animating circle.
     * @param endRadius The ending radius of the animating circle.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public static SupportAnimator createCircularReveal(View view,
                                                int centerX,  int centerY,
                                                float startRadius, float endRadius) {

        if(!(view.getParent() instanceof RevealAnimator)){
            throw new IllegalArgumentException("View must be inside RevealFrameLayout or RevealLinearLayout.");
        }

        RevealAnimator revealLayout = (RevealAnimator) view.getParent();
        revealLayout.attachRevealInfo(new RevealInfo(centerX, centerY, startRadius, endRadius,
                new WeakReference<>(view)));

        if(LOLLIPOP_PLUS){
            return new SupportAnimatorLollipop(android.view.ViewAnimationUtils
                    .createCircularReveal(view, centerX, centerY, startRadius, endRadius), revealLayout);
        }

        ObjectAnimator reveal = ObjectAnimator.ofFloat(revealLayout, CLIP_RADIUS,
                startRadius, endRadius);
        reveal.addListener(getRevealFinishListener(revealLayout));

        return new SupportAnimatorPreL(reveal, revealLayout);
    }

    private static Animator.AnimatorListener getRevealFinishListener(RevealAnimator target){
        if(SDK_INT >= 18){
            return new RevealAnimator.RevealFinishedJellyBeanMr2(target);
        }else if(SDK_INT >= 14){
            return new RevealAnimator.RevealFinishedIceCreamSandwich(target);
        }else {
            return new RevealAnimator.RevealFinishedGingerbread(target);
        }
    }

    /**
     * Lifting view
     *
     * @param view The animation target
     * @param baseRotation initial Rotation X in 3D space
     * @param fromY initial Y position of view
     * @param duration aniamtion duration
     * @param startDelay start delay before animation begin
     */
    @Deprecated
    public static void liftingFromBottom(View view, float baseRotation, float fromY, int duration, int startDelay){
        ViewHelper.setRotationX(view, baseRotation);
        ViewHelper.setTranslationY(view, fromY);

        ViewPropertyAnimator
                .animate(view)
                .setInterpolator(new AccelerateDecelerateInterpolator())
                .setDuration(duration)
                .setStartDelay(startDelay)
                .rotationX(0)
                .translationY(0)
                .start();

    }

    /**
     * Lifting view
     *
     * @param view The animation target
     * @param baseRotation initial Rotation X in 3D space
     * @param duration aniamtion duration
     * @param startDelay start delay before animation begin
     */
    @Deprecated
    public static void liftingFromBottom(View view, float baseRotation, int duration, int startDelay){
        ViewHelper.setRotationX(view, baseRotation);
        ViewHelper.setTranslationY(view, view.getHeight() / 3);

        ViewPropertyAnimator
                .animate(view)
                .setInterpolator(new AccelerateDecelerateInterpolator())
                .setDuration(duration)
                .setStartDelay(startDelay)
                .rotationX(0)
                .translationY(0)
                .start();

    }

    /**
     * Lifting view
     *
     * @param view The animation target
     * @param baseRotation initial Rotation X in 3D space
     * @param duration aniamtion duration
     */
    @Deprecated
    public static void liftingFromBottom(View view, float baseRotation, int duration){
        ViewHelper.setRotationX(view, baseRotation);
        ViewHelper.setTranslationY(view, view.getHeight() / 3);

        ViewPropertyAnimator
                .animate(view)
                .setInterpolator(new AccelerateDecelerateInterpolator())
                .setDuration(duration)
                .rotationX(0)
                .translationY(0)
                .start();

    }

    static class SimpleAnimationListener implements Animator.AnimatorListener{

        @Override
        public void onAnimationStart(Animator animation) {

        }

        @Override
        public void onAnimationEnd(Animator animation) {

        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    }

}