public final class Constants {

    public static final int MESSENGER_ID_CHANGE_THEME;
    public static final int MESSENGER_ID_UPDATE_RECORD;
    public static final int MESSENGER_ID_REPLY;
    public static final int MESSENGER_ID_CREATE_POST;
    public static final int MESSENGER_ID_FAST_SCROLLER;

    static {
        Messenger messenger = Messenger.getInstance();
        MESSENGER_ID_CHANGE_THEME = messenger.newId();
        MESSENGER_ID_UPDATE_RECORD = messenger.newId();
        MESSENGER_ID_REPLY = messenger.newId();
        MESSENGER_ID_CREATE_POST = messenger.newId();
        MESSENGER_ID_FAST_SCROLLER = messenger.newId();
    }
} public final class NMBApplication extends Application
        implements Thread.UncaughtExceptionHandler, Messenger.Receiver, Runnable {

    private static final String TAG = NMBApplication.class.getSimpleName();

    private static final boolean LOG_NATIVE_MEMORY = false;

    private static final String AC_CDN_PATH_FILENAME = "ac_cdn_path";

    private Thread.UncaughtExceptionHandler mDefaultHandler;

    private SimpleCookieStore mSimpleCookieStore;
    private NMBClient mNMBClient;
    private Conaco<ImageWrapper> mConaco;
    private ImageWrapperHelper mImageWrapperHelper;
    private OkHttpClient mOkHttpClient;

    private boolean mConnectedWifi;

    private List<WeakReference<Activity>> mActivities = new ArrayList<>();

    @Override
    public void onCreate() {
        super.onCreate();

        // Prepare to crash
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);

        NMBAppConfig.initialize(this);
        File logFile = NMBAppConfig.getFileInAppDir("nimingban.log");
        if (logFile != null) {
            Say.initSayFile(logFile);
        }
        Settings.initialize(this);
        DB.initialize(this);
        HttpCookieDB.initialize(this);
        ReadableTime.initialize(this);
        BitmapUtils.initialize(this);
        ResImageGetter.initialize(this);
        Emoji.initialize(this);

        // Remove temp file
        FileUtils.deleteContent(NMBAppConfig.getTempDir());

        // Check network state
        updateNetworkState(this);
        registerReceiver(new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                updateNetworkState(context);
            }
        }, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));

        // Theme
        setTheme(Settings.getDarkTheme() ? R.style.AppTheme_Dark : R.style.AppTheme);
        Messenger.getInstance().register(Constants.MESSENGER_ID_CHANGE_THEME, this);

        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                mActivities.add(new WeakReference<>(activity));
            }
            @Override
            public void onActivityDestroyed(Activity activity) {
                Iterator<WeakReference<Activity>> iterator = mActivities.iterator();
                while (iterator.hasNext()) {
                    WeakReference<Activity> reference = iterator.next();
                    Activity a = reference.get();
                    // Remove current activity and null
                    if (a == null || a == activity) {
                        iterator.remove();
                    }
                }
            }
            @Override
            public void onActivityStarted(Activity activity) {}
            @Override
            public void onActivityResumed(Activity activity) {}
            @Override
            public void onActivityPaused(Activity activity) {}
            @Override
            public void onActivityStopped(Activity activity) {}
            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
        });

        try {
            update();
        } catch (PackageManager.NameNotFoundException e) {
            // Ignore
        }

        if (LOG_NATIVE_MEMORY) {
            SimpleHandler.getInstance().post(this);
        }

        start();
    }

    private void start() {
        updateACCdnPath();
        updateACForums();
        updateACHost();

        long lastForumAging = Settings.getLastForumAging();
        long time = System.currentTimeMillis();
        if (time - lastForumAging > 24 * 60 * 60 * 1000) { // 24 hr * 60 min * 60 sec * 1000 milli
            ForumAutoSortingUtils.ageACForumFrequency();
            Settings.setLastForumAging(time);
        }
    }

    private void readACCdnPathFromFile() {
        File file = new File(getFilesDir(), AC_CDN_PATH_FILENAME);
        InputStream is = null;
        try {
            is = new FileInputStream(file);
            String str = IOUtils.readString(is, "utf-8");
            List<ACCdnPath> list = JSON.parseArray(str, ACCdnPath.class);
            ACSite.getInstance().setCdnPath(list);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(is);
        }
    }

    private void writeACCdnPathToFile(List<ACCdnPath> cdnPaths) {
        File file = new File(getFilesDir(), AC_CDN_PATH_FILENAME);
        OutputStream os = null;
        try {
            os = new FileOutputStream(file);
            os.write(JSON.toJSONString(cdnPaths).getBytes("utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            IOUtils.closeQuietly(os);
        }
    }

    private void updateACCdnPath() {
        // First read cdn path from file
        readACCdnPathFromFile();

        NMBRequest request = new NMBRequest();
        request.setSite(ACSite.getInstance());
        request.setMethod(NMBClient.METHOD_GET_CDN_PATH);
        request.setCallback(new NMBClient.Callback<List<ACCdnPath>>(){
            @Override
            public void onSuccess(List<ACCdnPath> result) {
                ACSite.getInstance().setCdnPath(result);
                writeACCdnPathToFile(result);
            }

            @Override
            public void onFailure(Exception e) {
                e.printStackTrace();
            }

            @Override
            public void onCancel() {
            }
        });
        getNMBClient(this).execute(request);
    }

    private void updateACForums() {
        NMBRequest request = new NMBRequest();
        request.setSite(ACSite.getInstance());
        request.setMethod(NMBClient.METHOD_GET_FORUM_LIST);
        request.setCallback(new NMBClient.Callback<List<ACForumGroup>>(){
            @Override
            public void onSuccess(List<ACForumGroup> result) {
                List<ACForum> list = new LinkedList<>();
                for (ACForumGroup forumGroup : result) {
                    list.addAll(forumGroup.forums);
                }
                DB.setACForums(list);
            }
            @Override
            public void onFailure(Exception e) { }
            @Override
            public void onCancel() { }
        });
        getNMBClient(this).execute(request);
    }

    @SuppressLint("StaticFieldLeak")
    private void updateACHost() {
        getNMBClient(this).execute(new AsyncTask<Object, Object, String>() {
            @Override
            protected String doInBackground(Object... objects) {
                OkHttpClient client = getOkHttpClient(NMBApplication.this);
                Request request = new Request.Builder().url("http://adnmb.com").build();
                Call call = client.newCall(request);
                try {
                    Response response = call.execute();
                    String url = response.request().url().toString();
                    response.close();

                    // Find third '/', make url like http://adnmb.com
                    int count = 0;
                    for (int i = 0, n = url.length(); i < n; i++) {
                        if (url.charAt(i) == '/') {
                            count++;
                        }
                        if (count == 3) {
                            url = url.substring(0, i);
                            break;
                        }
                    }

                    return url;
                } catch (IOException e) {
                    e.printStackTrace();
                    return null;
                }
            }

            @Override
            protected void onPostExecute(String host) {
                Log.d(TAG, "ac host = " + host);
                Settings.putAcHost(host);
            }
        });
    }

    private void update() throws PackageManager.NameNotFoundException {
        int oldVersionCode = Settings.getVersionCode();
        PackageInfo pi = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
        Settings.putVersionCode(pi.versionCode);

        if (oldVersionCode < 6) {
            updateCookies(this);
        }

        if (oldVersionCode < 14) {
            Settings.putGuideListActivity(true);
        }

        if (oldVersionCode < 42) {
            Settings.putSetAnalysis(false);
            Settings.putAnalysis(false);
        }

        // Fix cookie lost when save to file in 1.2.29 and below
        if (oldVersionCode < 44) {
            NMBApplication.getSimpleCookieStore(this).fixLostCookiePath();
        }
    }

    public static void updateCookies(Context context) {
        SimpleCookieStore cookieStore = NMBApplication.getSimpleCookieStore(context);

        URL url = ACSite.getInstance().getSiteUrl();
        HttpCookieWithId hcwi = cookieStore.getCookie(url, "userId");
        if (hcwi != null) {
            HttpCookie oldCookie = hcwi.httpCookie;
            cookieStore.remove(url, oldCookie);

            HttpCookie newCookie = new HttpCookie("userhash", oldCookie.getValue());
            newCookie.setComment(oldCookie.getComment());
            newCookie.setCommentURL(oldCookie.getCommentURL());
            newCookie.setDiscard(oldCookie.getDiscard());
            newCookie.setDomain(oldCookie.getDomain());
            newCookie.setMaxAge(oldCookie.getMaxAge());
            newCookie.setPath(oldCookie.getPath());
            newCookie.setPortlist(oldCookie.getPortlist());
            newCookie.setSecure(oldCookie.getSecure());
            newCookie.setVersion(oldCookie.getVersion());

            cookieStore.add(url, newCookie);
        }
    }

    public void bringActivitiesToForeground() {
        int i = mActivities.size();
        while (--i >= 0) {
            Activity activity = mActivities.get(i).get();
            if (activity != null && !"com.hippo.nimingban.wxapi.WXEntryActivity".equals(activity.getClass().getName())) {
                Intent intent = new Intent(activity, BringToForegroundActivity.class);
                activity.startActivity(intent);
            }
        }
    }

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);

        if (level ==  TRIM_MEMORY_BACKGROUND ) {
            if (mConaco != null) {
                mConaco.clearMemoryCache();
            }
        }
    }

    public static void updateNetworkState(Context context) {
        ((NMBApplication) context.getApplicationContext()).mConnectedWifi =
                NetworkUtils.isConnectedWifi(context);
    }

    public static boolean isConnectedWifi(Context context) {
        return ((NMBApplication) context.getApplicationContext()).mConnectedWifi;
    }

    public static SimpleCookieStore getSimpleCookieStore(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mSimpleCookieStore == null) {
            application.mSimpleCookieStore = new SimpleCookieStore();
        }
        return application.mSimpleCookieStore;
    }

    @NonNull
    public static NMBClient getNMBClient(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mNMBClient == null) {
            application.mNMBClient = new NMBClient(application);
        }
        return application.mNMBClient;
    }

    private static int getMemoryCacheMaxSize(Context context) {
        final ActivityManager activityManager = (ActivityManager) context.
                getSystemService(Context.ACTIVITY_SERVICE);
        return Math.min(20 * 1024 * 1024,
                Math.round(0.2f * activityManager.getMemoryClass() * 1024 * 1024));
    }

    @NonNull
    public static Conaco<ImageWrapper> getConaco(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mConaco == null) {
            Conaco.Builder<ImageWrapper> builder = new Conaco.Builder<>();
            builder.hasMemoryCache = true;
            builder.memoryCacheMaxSize = getMemoryCacheMaxSize(context);
            builder.hasDiskCache = true;
            builder.diskCacheDir = new File(context.getCacheDir(), "thumb");
            builder.diskCacheMaxSize = 80 * 1024 * 1024; // 80MB
            builder.okHttpClient = getOkHttpClient(context);
            builder.objectHelper = getImageWrapperHelper(context);
            application.mConaco = builder.build();
        }
        return application.mConaco;
    }

    @NonNull
    public static ImageWrapperHelper getImageWrapperHelper(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mImageWrapperHelper == null) {
            application.mImageWrapperHelper = new ImageWrapperHelper();
        }
        return application.mImageWrapperHelper;
    }

    public static OkHttpClient getOkHttpClient(@NonNull Context context) {
        NMBApplication application = ((NMBApplication) context.getApplicationContext());
        if (application.mOkHttpClient == null) {
            application.mOkHttpClient = new OkHttpClient.Builder()
                    .connectTimeout(15, TimeUnit.SECONDS)
                    .readTimeout(15, TimeUnit.SECONDS)
                    .writeTimeout(15, TimeUnit.SECONDS)
                    .dns(new NMBDns())
                    .cookieJar(new CookieDBJar(getSimpleCookieStore(context)))
                    .addInterceptor(new NMBInterceptor())
                    .build();
        }
        return application.mOkHttpClient;
    }

    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        if (!handleException(ex) && mDefaultHandler != null) {
            mDefaultHandler.uncaughtException(thread, ex);
        }
        android.os.Process.killProcess(android.os.Process.myPid());
        System.exit(1);
    }

    private boolean handleException(Throwable ex) {
        if (ex == null) {
            return false;
        }
        try {
            ex.printStackTrace();
            Crash.saveCrashInfo2File(this, ex);
            return true;
        } catch (Throwable tr) {
            return false;
        }
    }

    @Override
    public void onReceive(int id, Object obj) {
        setTheme((Boolean) obj ? R.style.AppTheme_Dark : R.style.AppTheme);
    }

    @Override
    public void run() {
        Log.i(TAG, "Native " + FileUtils.humanReadableByteCount(Debug.getNativeHeapAllocatedSize(), false));
        SimpleHandler.getInstance().postDelayed(this, 3000);
    }
} public class NMBClient {

    public static final String TAG = NMBClient.class.getSimpleName();

    public static final int METHOD_NOTICE = -6;

    public static final int METHOD_CONVERT = -5;

    public static final int METHOD_COMMON_POSTS = -4;

    public static final int METHOD_DISC = -3;

    public static final int METHOD_UPDATE = -2;

    public static final int METHOD_GET_FORUM_LIST = -1;

    public static final int METHOD_GET_COOKIE = 0;

    public static final int METHOD_GET_POST_LIST = 1;

    public static final int METHOD_GET_POST = 2;

    public static final int METHOD_GET_REFERENCE = 3;

    public static final int METHOD_REPLY = 4;

    public static final int METHOD_GET_FEED = 5;

    public static final int METHOD_ADD_FEED = 6;

    public static final int METHOD_DEL_FEED = 7;

    public static final int METHOD_CREATE_POST = 8;

    public static final int METHOD_SEARCH = 10;

    public static final int METHOD_GET_CDN_PATH = 11;

    private final ThreadPoolExecutor mRequestThreadPool;
    private final OkHttpClient mOkHttpClient;

    public NMBClient(Context context) {
        ThreadFactory threadFactory = new PriorityThreadFactory(TAG,
                android.os.Process.THREAD_PRIORITY_BACKGROUND);
        mRequestThreadPool = FixedThreadPoolExecutor.newInstance(
            3, 32, 1L, TimeUnit.SECONDS, threadFactory);
        mOkHttpClient = NMBApplication.getOkHttpClient(context);
    }

    public void execute(NMBRequest request) {
        if (!request.isCancelled()) {
            Task task = new Task(request.method, request.site, request.callback);
            task.executeOnExecutor(mRequestThreadPool, request.args);
            request.task = task;
        } else {
            request.callback.onCancel();
        }
    }

    public void execute(AsyncTask<?, ?, ?> task) {
        task.executeOnExecutor(mRequestThreadPool);
    }

    public class Task extends AsyncTask<Object, Void, Object> {

        private int mMethod;
        private Site mSite;
        private Callback mCallback;
        private Call mCall;

        private boolean mStop;

        public Task(int method, Site site, Callback callback) {
            mMethod = method;
            mSite = site;
            mCallback = callback;
        }

        public void stop() {
            if (!mStop) {
                mStop = true;

                if (mCallback != null) {
                    final Callback finalCallback = mCallback;
                    SimpleHandler.getInstance().post(new Runnable() {
                        @Override
                        public void run() {
                            finalCallback.onCancel();
                        }
                    });
                }

                Status status = getStatus();
                if (status == Status.PENDING) {
                    cancel(false);
                } else if (status == Status.RUNNING) {
                    if (mCall != null) {
                        mCall.cancel();
                    }
                }

                // Clear
                mCall = null;
                mCallback = null;
            }
        }

        private Object getCommonPosts() throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareGetCommonPosts(mOkHttpClient);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doGetCommonPosts(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object getForumList() throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareGetForumList(mOkHttpClient);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doGetForumList(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object getCookie() throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareGetCookie(mOkHttpClient);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doGetCookie(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object getPostList(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareGetPostList(mOkHttpClient, (String) params[0], (Integer) params[1]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doGetPostList(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object getPost(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareGetPost(mOkHttpClient, (String) params[0], (Integer) params[1]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doGetPost(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object getReference(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareGetReference(mOkHttpClient, (String) params[0]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doGetReference(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object reply(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareReply(mOkHttpClient, (ACReplyStruct) params[0]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doReply(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }


        private Object getFeed(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareGetFeed(mOkHttpClient, (String) params[0], (Integer) params[1]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doGetFeed(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object addFeed(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareAddFeed(mOkHttpClient, (String) params[0], (String) params[1]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doAddFeed(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object delFeed(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareDelFeed(mOkHttpClient, (String) params[0], (String) params[1]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doDelFeed(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object createPost(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareCreatePost(mOkHttpClient, (ACPostStruct) params[0]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doCreatePost(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object search(Object... params) throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareSearch(mOkHttpClient, (String) params[0], (Integer) params[1]);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doSearch(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        private Object getCdnPath() throws Exception {
            switch (mSite.getId()) {
                case Site.AC:
                    Call call = ACEngine.prepareGetCdnPath(mOkHttpClient);
                    if (!mStop) {
                        mCall = call;
                        return ACEngine.doGetCdnPath(call);
                    } else {
                        throw new CancelledException();
                    }
                default:
                    return new IllegalStateException("Can't detect site " + mSite);
            }
        }

        @Override
        protected Object doInBackground(Object... params) {
            try {
                switch (mMethod) {
                    case METHOD_NOTICE: {
                        Log.d(TAG, "http://cover.acfunwiki.org/nmb-notice.json");
                        Request request = new Request.Builder().url("http://cover.acfunwiki.org/nmb-notice.json").build();
                        Call call = mOkHttpClient.newCall(request);
                        if (!mStop) {
                            mCall = call;
                            Response response = call.execute();
                            Notice notice = JSON.parseObject(response.body().string(), Notice.class);
                            if (notice == null) {
                                throw new Exception("No notice");
                            }
                            return notice;
                        } else {
                            throw new CancelledException();
                        }
                    }
                    case METHOD_CONVERT: {
                        Call call = ConvertEngine.prepareConvert(mOkHttpClient, (String) params[0], (String) params[1]);
                        if (!mStop) {
                            mCall = call;
                            return ConvertEngine.doConvert(call);
                        } else {
                            throw new CancelledException();
                        }
                    }
                    case METHOD_COMMON_POSTS:
                        return getCommonPosts();
                    case METHOD_DISC:
                        return DiscEngine.spider(mOkHttpClient, (String) params[0], (String) params[1]);
                    case METHOD_UPDATE: {
                        Call call = UpdateEngine.prepareUpdate(mOkHttpClient);
                        if (!mStop) {
                            mCall = call;
                            return UpdateEngine.doUpdate(call);
                        } else {
                            throw new CancelledException();
                        }
                    }
                    case METHOD_GET_FORUM_LIST:
                        return getForumList();
                    case METHOD_GET_COOKIE:
                        return getCookie();
                    case METHOD_GET_POST_LIST:
                        return getPostList(params);
                    case METHOD_GET_POST:
                        return getPost(params);
                    case METHOD_GET_REFERENCE:
                        return getReference(params);
                    case METHOD_REPLY:
                        return reply(params);
                    case METHOD_GET_FEED:
                        return getFeed(params);
                    case METHOD_ADD_FEED:
                        return addFeed(params);
                    case METHOD_DEL_FEED:
                        return delFeed(params);
                    case METHOD_CREATE_POST:
                        return createPost(params);
                    case METHOD_SEARCH:
                        return search(params);
                    case METHOD_GET_CDN_PATH:
                        return getCdnPath();
                    default:
                        return new IllegalStateException("Can't detect method " + mMethod);
                }
            } catch (Exception e) {
                return e;
            }
        }

        @SuppressWarnings("unchecked")
        @Override
        protected void onPostExecute(Object result) {
            if (mCallback != null) {
                if (!(result instanceof CancelledException)) {
                    if (result instanceof Exception) {
                        mCallback.onFailure((Exception) result);
                    } else {
                        mCallback.onSuccess(result);
                    }
                } else {
                    // onCancel is called in stop
                }
            }

            // Clear
            mCall = null;
            mCallback = null;
        }
    }

    public interface Callback<E> {

        void onSuccess(E result);

        void onFailure(Exception e);

        void onCancel();
    }
} public final class ACItemUtils {
    private ACItemUtils() {}

    private static final Pattern REFERENCE_PATTERN = Pattern.compile(">>?(?:No.)?(\\d+)");
    private static final Pattern URL_PATTERN = Pattern.compile("(http|https)://[a-z0-9A-Z%-]+(\\.[a-z0-9A-Z%-]+)+(:\\d{1,5})?(/[a-zA-Z0-9-_~:#@!&',;=%/\\*\\.\\?\\+\\$\\[\\]\\(\\)]+)?/?");
    private static final Pattern AC_PATTERN = Pattern.compile("ac\\d+");
    private static final Pattern HIDE_PATTERN = Pattern.compile("\\[h](.+?)\\[/h]");

    private static final String NO_TITLE = "无标题";
    private static final String NO_NAME = "无名氏";

    private static final int COUNT_NUMBER = '9' - '0' + 1;
    private static final int COUNT_UPPERCASE_LETTER = 'z' - 'a' + 1;
    private static final int COUNT_LOWERCASE_LETTER = 'Z' - 'A' + 1;
    private static final int COUNT_SUM = COUNT_NUMBER + COUNT_UPPERCASE_LETTER + COUNT_LOWERCASE_LETTER;

    private static final byte[] IV = new byte[] {0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF};

    private static final String NMB_HIDE_TAG = "nmb-hide";
    private static final String NMB_HIDE_TAG_LEFT = "<" + NMB_HIDE_TAG + ">";
    private static final String NMB_HIDE_TAG_RIGHT = "</" + NMB_HIDE_TAG + ">";

    private static final ACHtmlTagHandler AC_HTML_TAG_HANDLER = new ACHtmlTagHandler();

    private static String replaceBracketsH(String content) {
        Matcher m = HIDE_PATTERN.matcher(content);

        int lastEnd = 0;
        StringBuilder sb = null;

        while (m.find()) {
            if (sb == null) sb = new StringBuilder(content.length());
            sb.append(content, lastEnd, m.start());
            sb.append(NMB_HIDE_TAG_LEFT);
            sb.append(m.group(1));
            sb.append(NMB_HIDE_TAG_RIGHT);
            lastEnd = m.end();
        }

        if (sb != null) {
            sb.append(content.substring(lastEnd));
            return sb.toString();
        } else {
            return content;
        }
    }

    public static CharSequence handleReference(CharSequence content) {
        Matcher m = REFERENCE_PATTERN.matcher(content);

        Spannable spannable = null;
        while (m.find()) {
            // Ensure spannable
            if (spannable == null) {
                if (content instanceof Spannable) {
                    spannable = (Spannable) content;
                } else {
                    spannable = new SpannableString(content);
                }
            }

            int start = m.start();
            int end = m.end();

            ReferenceSpan referenceSpan = new ReferenceSpan(ACSite.getInstance(), m.group(1));
            spannable.setSpan(referenceSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        return spannable == null ? content : spannable;
    }

    public static CharSequence handleTextUrl(CharSequence content) {
        Matcher m = URL_PATTERN.matcher(content);

        Spannable spannable = null;
        while (m.find()) {
            // Ensure spannable
            if (spannable == null) {
                if (content instanceof Spannable) {
                    spannable = (Spannable) content;
                } else {
                    spannable = new SpannableString(content);
                }
            }

            int start = m.start();
            int end = m.end();

            URLSpan[] links = spannable.getSpans(start, end, URLSpan.class);
            if (links.length > 0) {
                // There has been URLSpan already, leave it alone
                continue;
            }

            URLSpan urlSpan = new URLSpan(m.group(0));
            spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        return spannable == null ? content : spannable;
    }

    public static CharSequence handleAcUrl(CharSequence content) {
        Matcher m = AC_PATTERN.matcher(content);

        Spannable spannable = null;
        while (m.find()) {
            // Ensure spannable
            if (spannable == null) {
                if (content instanceof Spannable) {
                    spannable = (Spannable) content;
                } else {
                    spannable = new SpannableString(content);
                }
            }

            int start = m.start();
            int end = m.end();

            URLSpan[] links = spannable.getSpans(start, end, URLSpan.class);
            if (links.length > 0) {
                // There has been URLSpan already, leave it alone
                continue;
            }

            URLSpan urlSpan = new URLSpan("http://www.acfun.cn/v/" + m.group(0));
            spannable.setSpan(urlSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        return spannable == null ? content : spannable;
    }

    public static CharSequence generateContent(String content) {
        content = replaceBracketsH(content);
        CharSequence charSequence;
        charSequence = Html.fromHtml(content, null, AC_HTML_TAG_HANDLER);
        charSequence = handleReference(charSequence);
        charSequence = handleTextUrl(charSequence);
        charSequence = handleAcUrl(charSequence);

        return charSequence;
    }

    public static CharSequence generateContent(String content, String sage, String title, String name) {
        StringBuilder sb = new StringBuilder(44 + 11 + StringUtils.length(title) +
                11 + StringUtils.length(name) +
                StringUtils.length(content));
        if ("1".equals(sage)) {
            sb.append("<font color=\"red\"><b>SAGE</b></font><br><br>");
        }
        if (!TextUtils.isEmpty(title) && !NO_TITLE.equals(title)) {
            sb.append("<b>").append(title).append("</b><br>");
        }
        if (!TextUtils.isEmpty(name) && !NO_NAME.equals(name)) {
            sb.append("<b>").append(name).append("</b><br>");
        }
        sb.append(content);

        return generateContent(sb.toString());
    }

    private static char getReadableChar(byte b) {
        int i = (b & 0xFF) % COUNT_SUM;
        if (i < COUNT_NUMBER) {
            return (char) ('0' + i);
        } else if (i < COUNT_NUMBER + COUNT_UPPERCASE_LETTER) {
            return (char) ('a' + i - COUNT_NUMBER);
        } else if (i < COUNT_NUMBER + COUNT_UPPERCASE_LETTER + COUNT_LOWERCASE_LETTER) {
            return (char) ('A' + i - COUNT_NUMBER - COUNT_UPPERCASE_LETTER);
        } else {
            return '?';
        }
    }

    private static String toReadableString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length);
        for (byte b: bytes) {
            sb.append(getReadableChar(b));
        }
        return sb.toString();
    }

    private static boolean isSimpleUser(@Nullable String userID) {
        if (null == userID) {
            return false;
        }

        for (int i = 0, n = userID.length(); i < n; i++) {
            char ch = userID.charAt(i);
            if (!(ch >= '0' && ch <= '9') && !(ch >= 'a' && ch <= 'z') && !(ch >= 'A' && ch <= 'Z')) {
                return false;
            }
        }

        return true;
    }

    public static CharSequence handleUser(CharSequence user, String postId, String id) {
        String key;

        switch (Settings.getChaosLevel()) {
            case 1: // Relatively chaotic
                key = postId;
                break;
            case 2: // Absolutely chaotic
                key = id;
                break;
            default:
                return user;
        }

        String userStr = user.toString();

        if (!isSimpleUser(userStr)) {
            return user;
        }

        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(key.getBytes("utf-8"));
            byte[] keyBytes = digest.digest();
            byte[] input = userStr.getBytes("utf-8");

            IvParameterSpec ivSpec = new IvParameterSpec(IV);
            SecretKeySpec skeySpec = new SecretKeySpec(keyBytes, "AES");
            Cipher cipher = Cipher.getInstance("AES/CFB/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivSpec);

            byte[] cipherText = new byte[cipher.getOutputSize(input.length)];
            int ctLength = cipher.update(input, 0, input.length, cipherText, 0);
            cipher.doFinal(cipherText, ctLength);

            return toReadableString(cipherText);
        } catch (Exception e) {
            e.printStackTrace();
            return user;
        }
    }

    private static class ACHtmlTagHandler implements Html.TagHandler {
        @Override
        public boolean handleTag(boolean opening, String tag,
                SpannableStringBuilder output, XMLReader xmlReader, Attributes attributes) {
            if (tag.equalsIgnoreCase("a")) {
                if (opening) {
                    // Add ac nmb host
                    String href = attributes.getValue("", "href");
                    if (!href.startsWith("http")) {
                        if (href.startsWith("/")) {
                            href = ACUrl.getHost() + href;
                        } else {
                            href = ACUrl.getHost() + '/' + href;
                        }
                    }
                    int len = output.length();
                    output.setSpan(new Href(href), len, len, Spannable.SPAN_MARK_MARK);
                } else {
                    int len = output.length();
                    Object obj = getLast(output, Href.class);
                    int where = output.getSpanStart(obj);
                    output.removeSpan(obj);
                    if (where != len) {
                        Href h = (Href) obj;
                        if (h.mHref != null) {
                            output.setSpan(new URLSpan(h.mHref), where, len,
                                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        }
                    }
                }
                return true;
            } else if (tag.equalsIgnoreCase(NMB_HIDE_TAG)) {
                if (opening) {
                    int len = output.length();
                    output.setSpan(new Hide(), len, len, Spannable.SPAN_MARK_MARK);
                } else {
                    int len = output.length();
                    Object obj = getLast(output, Hide.class);
                    int where = output.getSpanStart(obj);

                    output.removeSpan(obj);

                    if (where != len) {
                        output.setSpan(new HideSpan(where, len), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                }
                return true;
            } else {
                return false;
            }
        }
    }

    private static Object getLast(Spanned text, Class kind) {
        /*
         * This knows that the last returned object from getSpans()
         * will be the most recently added.
         */
        Object[] objs = text.getSpans(0, text.length(), kind);

        if (objs.length == 0) {
            return null;
        } else {
            return objs[objs.length - 1];
        }
    }

    private static class Href {
        public String mHref;

        public Href(String href) {
            mHref = href;
        }
    }

    public static void setContentText(TextView tv, CharSequence text) {
        tv.setText(updateContentText(text, tv.getTextColors().getDefaultColor()));
    }

    public static CharSequence updateContentText(CharSequence text, int hideColor) {
        if (text instanceof Spanned) {
            Spanned spanned = (Spanned) text;
            ACItemUtils.HideSpan[] hideSpans = spanned.getSpans(0, spanned.length(), ACItemUtils.HideSpan.class);
            for (ACItemUtils.HideSpan hideSpan : hideSpans) {
                spanned = hideSpan.update(spanned, hideColor);
            }
            text = spanned;
        }
        return text;
    }


    private static class Hide {
        public Hide() { }
    }

    public static class HideSpan {

        public final int start;
        public final int end;
        public boolean hidden;
        public BackgroundColorSpan hideSpan1;
        public final ForegroundColorSpan hideSpan2;

        public HideSpan(int start, int end) {
            this.start = start;
            this.end = end;
            this.hidden = true;
            this.hideSpan2 = new ForegroundColorSpan(Color.TRANSPARENT);
        }

        private BackgroundColorSpan updateHideSpan1(int hideColor) {
            if (hideSpan1 != null && hideSpan1.getBackgroundColor() == hideColor) return null;
            BackgroundColorSpan oldHideSpan = hideSpan1;
            hideSpan1 = new BackgroundColorSpan(hideColor);
            return oldHideSpan;
        }

        private Spanned checkTextView(TextView tv) {
            CharSequence text = tv.getText();
            if (!(text instanceof Spanned)) return null;
            return (Spanned) text;
        }

        public Spanned update(Spanned spanned, int hideColor) {
            if (spanned.getSpanStart(this) == -1) return spanned;

            ACItemUtils.Text text = new ACItemUtils.Text(spanned);

            BackgroundColorSpan oldHideSpan = updateHideSpan1(hideColor);
            if (oldHideSpan != null) text.removeSpan(oldHideSpan);

            if (hidden) {
                text.setSpan(hideSpan1, start, end);
                text.setSpan(hideSpan2, start, end);
            } else {
                text.removeSpan(hideSpan1);
                text.removeSpan(hideSpan2);
            }

            return text.getText();
        }

        public void update(TextView tv) {
            Spanned spanned = checkTextView(tv);
            if (spanned == null) return;

            CharSequence text = update(spanned, tv.getTextColors().getDefaultColor());

            if (text != spanned) tv.setText(text);
        }

        public void toggle(TextView tv) {
            Spanned spanned = checkTextView(tv);
            if (spanned == null) return;

            hidden = !hidden;

            update(tv);
        }
    }

    private static class Text {
        private final Spanned spanned;
        private Spannable spannable;

        Text(Spanned spanned) {
            this.spanned = spanned;
            if (spanned instanceof Spannable) spannable = (Spannable) spanned;
        }

        void removeSpan(Object what) {
            if (spannable == null && spanned.getSpanStart(what) != -1)
                spannable = new SpannableString(spanned);
            if (spannable != null) spannable.removeSpan(what);
        }

        void setSpan(Object what, int start, int end) {
            if (spannable == null && (spanned.getSpanStart(what) != start || spanned.getSpanEnd(what) != end))
                spannable = new SpannableString(spanned);
            if (spannable != null) spannable.setSpan(what, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        Spanned getText() {
            return spannable != null ? spannable : spanned;
        }
    }
} public class ACSite extends Site {

    private static final String TAG = ACSite.class.getSimpleName();

    private static final String DEFAULT_PICTURE_PREFIX = ACUrl.getHost() + "/Public/Upload/";

    private static final List<ACCdnPath> DEFAULT_AC_CDN_PATH_LIST = Collections.singletonList(new ACCdnPath());
    private static final String[] DEFAULT_AC_CDN_HOST_LIST = { ACCdnPath.DEFAULT_CDN_HOST };

    private URL mSiteUrl;

    private List<ACCdnPath> mCdnPathList = DEFAULT_AC_CDN_PATH_LIST;
    private float mRateSum;

    private boolean mCdnHostsDirty;
    private String[] mCdnHosts = DEFAULT_AC_CDN_HOST_LIST;

    private static ACSite sInstance;

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

    private ACSite() {
        try {
            mSiteUrl = new URL(ACUrl.getHost());
        } catch (MalformedURLException e) {
            throw new IllegalStateException(e);
        }
    }

    public URL getSiteUrl() {
        return mSiteUrl;
    }

    @Override
    public int getId() {
        return Site.AC;
    }

    @Override
    public String getReadableName(Context context) {
        return "ac";
    }

    @Override
    public long getCookieMaxAge(Context context) {
        SimpleCookieStore cookieStore = NMBApplication.getSimpleCookieStore(context);
        HttpCookieWithId cookie = cookieStore.getCookie(mSiteUrl, "userhash");
        if (cookie == null) {
            return -2;
        } else {
            return cookie.getMaxAge();
        }
    }

    @Override
    public void setCookieMaxAge(Context context, long maxAge) {
        SimpleCookieStore cookieStore = NMBApplication.getSimpleCookieStore(context);
        HttpCookieWithId cookie = cookieStore.getCookie(mSiteUrl, "userhash");
        if (cookie != null) {
            // Remove it
            cookieStore.remove(mSiteUrl, "userhash");

            // Update it
            HttpCookie httpCookie = cookie.httpCookie;
            httpCookie.setMaxAge(maxAge);
            cookieStore.add(mSiteUrl, httpCookie);
        }
    }

    @Override
    public String getUserId(Context context) {
        return Settings.getFeedId();
    }

    @Override
    public String getPostTitle(Context context, String postId) {
        return "No." + postId;
    }

    @Override
    public String getReportForumId() {
        return "18"; // TODO how to get it ?
    }

    public synchronized void setCdnPath(List<ACCdnPath> list) {
        if (list == null || list.isEmpty()) {
            return;
        }

        // Remove invalid cdn path
        List<ACCdnPath> paths = new ArrayList<>(list.size());
        for (ACCdnPath path : list) {
            if (path.url != null && HttpUrl.parse(path.url) != null && path.rate > 0) {
                paths.add(path);
            }
        }
        if (paths.isEmpty()) {
            return;
        }

        mCdnPathList = paths;

        mRateSum = 0.0f;
        for (int i = 0, size = list.size(); i < size; i++) {
            mRateSum += list.get(i).rate;
        }

        // Set cdn hosts dirty
        mCdnHostsDirty = true;
    }

    private ACCdnPath getCdnPath() {
        final float r = MathUtils.random(mRateSum);
        float sum = 0.0f;
        List<ACCdnPath> list = mCdnPathList;
        ACCdnPath cdnPath = null;
        for (int i = 0, size = list.size(); i < size; i++) {
            cdnPath = list.get(i);
            sum += cdnPath.rate;
            if (r <= sum) {
                return cdnPath;
            }
        }
        return cdnPath;
    }

    @NonNull
    public String[] getCdnHosts() {
        if (mCdnHostsDirty || mCdnHosts == null) {
            if (mCdnPathList == null) {
                mCdnHosts = new String[0];
            } else {
                List<String> hosts = new ArrayList<>();
                for (ACCdnPath cdn : mCdnPathList) {
                    if (cdn.url == null) {
                        continue;
                    }
                    HttpUrl url = HttpUrl.parse(cdn.url);
                    if (url != null) {
                        hosts.add(url.host());
                    }
                }
                mCdnHosts = hosts.toArray(new String[hosts.size()]);
            }
        }
        return mCdnHosts;
    }

    public synchronized String getPictureUrl(String key) {
        String url;
        ACCdnPath cdnPath;

        if (mCdnPathList != null && (cdnPath = getCdnPath()) != null) {
            url = UrlUtils.join(cdnPath.url, key);
        } else {
            url = DEFAULT_PICTURE_PREFIX + key;
        }

        return url;
    }
} public abstract class Post extends Reply {

    public abstract String getNMBFid();

    public abstract int getNMBReplyCount();

    public abstract CharSequence getNMBReplyDisplayCount();

    public abstract Reply[] getNMBReplies();
} public abstract class Site {

    public static final int RANDOM = 0;
    public static final int AC = 1;
    public static final int SITE_MIN = AC;
    public static final int SITE_MAX = AC;

    public static Site fromId(int id) {
        switch (id) {
            case RANDOM:
                return fromId(MathUtils.random(SITE_MIN, SITE_MAX + 1));
            case AC:
                return ACSite.getInstance();
            default:
                throw new IllegalStateException("Unknown site " + id);
        }
    }

    public static boolean isValid(int id) {
        return id >= SITE_MIN && id <= SITE_MAX;
    }

    public abstract int getId();

    public abstract String getReadableName(Context context);

    /**
     * @return 0 or negative for none
     */
    public abstract long getCookieMaxAge(Context context);

    public abstract void setCookieMaxAge(Context context, long maxAge);

    public abstract String getUserId(Context context);

    public abstract String getPostTitle(Context context, String postId);

    public abstract String getReportForumId();
} public final class ReadableTime {

    private static Resources sResources;

    public static final long SECOND_MILLIS = 1000;
    public static final long MINUTE_MILLIS = 60 * SECOND_MILLIS;
    public static final long HOUR_MILLIS = 60 * MINUTE_MILLIS;
    public static final long DAY_MILLIS = 24 * HOUR_MILLIS;
    public static final long WEEK_MILLIS = 7 * DAY_MILLIS;
    public static final long YEAR_MILLIS = 365 * DAY_MILLIS;

    public static final int SIZE = 5;

    public static final long[] MULTIPLES = {
            YEAR_MILLIS,
            DAY_MILLIS,
            HOUR_MILLIS,
            MINUTE_MILLIS,
            SECOND_MILLIS
    };

    public static final int[] UNITS = {
            R.plurals.year,
            R.plurals.day,
            R.plurals.hour,
            R.plurals.minute,
            R.plurals.second
    };

    private static final Calendar sCalendar = Calendar.getInstance(TimeZone.getTimeZone("GMT+08:00"));
    private static final Object sCalendarLock = new Object();

    private static final SimpleDateFormat DATE_FORMAT_WITHOUT_YEAR =
            new SimpleDateFormat("MMM d", Locale.getDefault());

    private static final SimpleDateFormat DATE_FORMAT_WIT_YEAR =
            new SimpleDateFormat("yyy MMM d", Locale.getDefault());

    private static final SimpleDateFormat DATE_FORMAT =
            new SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault());
    private static final Object sDateFormatLock1 = new Object();

    private static final SimpleDateFormat FILENAMABLE_DATE_FORMAT =
            new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.getDefault());
    private static final Object sDateFormatLock2 = new Object();

    static {
        // The website use GMT+08:00, so tell user the same
        DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+08:00"));

        DATE_FORMAT_WITHOUT_YEAR.setTimeZone(TimeZone.getTimeZone("GMT+08:00"));
    }

    public static void initialize(Context context) {
        sResources = context.getApplicationContext().getResources();
    }

    public static String getDisplayTime(long time) {
        if (Settings.getPrettyTime()) {
            return getTimeAgo(time);
        } else {
            return getPlainTime(time);
        }
    }

    public static String getPlainTime(long time) {
        synchronized (sDateFormatLock1) {
            return DATE_FORMAT.format(new Date(time));
        }
    }

    public static String getTimeAgo(long time) {
        Resources resources = sResources;

        long now = System.currentTimeMillis();
        if (time > now + (2 * MINUTE_MILLIS) || time <= 0) {
            return resources.getString(R.string.from_the_future);
        }

        final long diff = now - time;
        if (diff < MINUTE_MILLIS) {
            return resources.getString(R.string.just_now);
        } else if (diff < 2 * MINUTE_MILLIS) {
            return resources.getQuantityString(R.plurals.some_minutes_ago, 1, 1);
        } else if (diff < 50 * MINUTE_MILLIS) {
            int minutes = (int) (diff / MINUTE_MILLIS);
            return resources.getQuantityString(R.plurals.some_minutes_ago, minutes, minutes);
        } else if (diff < 90 * MINUTE_MILLIS) {
            return resources.getQuantityString(R.plurals.some_hours_ago, 1, 1);
        } else if (diff < 24 * HOUR_MILLIS) {
            int hours = (int) (diff / HOUR_MILLIS);
            return resources.getQuantityString(R.plurals.some_hours_ago, hours, hours);
        } else if (diff < 48 * HOUR_MILLIS) {
            return resources.getString(R.string.yesterday);
        } else if (diff < WEEK_MILLIS) {
            int days = (int) (diff / DAY_MILLIS);
            return resources.getString(R.string.some_days_ago, days);
        } else {
            synchronized (sCalendarLock) {
                Date nowDate = new Date(now);
                Date timeDate = new Date(time);
                sCalendar.setTime(nowDate);
                int nowYear = sCalendar.get(Calendar.YEAR);
                sCalendar.setTime(timeDate);
                int timeYear = sCalendar.get(Calendar.YEAR);

                if (nowYear == timeYear) {
                    return DATE_FORMAT_WITHOUT_YEAR.format(timeDate);
                } else {
                    return DATE_FORMAT_WIT_YEAR.format(timeDate);
                }
            }
        }
    }

    public static String getTimeInterval(long time) {
        StringBuilder sb = new StringBuilder();
        Resources resources = sResources;

        long leftover = time;
        boolean start = false;

        for (int i = 0; i < SIZE; i++) {
            long multiple = MULTIPLES[i];
            long quotient = leftover / multiple;
            long remainder = leftover % multiple;
            if (start || quotient != 0 || i == SIZE - 1) {
                if (start) {
                    sb.append(" ");
                }
                sb.append(quotient)
                        .append(" ")
                        .append(resources.getQuantityString(UNITS[i], (int) quotient));
                start = true;
            }
            leftover = remainder;
        }

        return sb.toString();
    }

    public static String getFilenamableTime(long time) {
        synchronized (sDateFormatLock2) {
            return FILENAMABLE_DATE_FORMAT.format(new Date(time));
        }
    }
} public final class Settings {

    private static Context sContext;
    private static SharedPreferences sSettingsPre;

    private static boolean sCheckExtendFeedId = false;
    private static String sExtendFeedId = null;

    public static void initialize(Context context) {
        sContext = context.getApplicationContext();
        sSettingsPre = PreferenceManager.getDefaultSharedPreferences(sContext);
    }

    public static boolean getBoolean(String key, boolean defValue) {
        return sSettingsPre.getBoolean(key, defValue);
    }

    public static void putBoolean(String key, boolean value) {
        sSettingsPre.edit().putBoolean(key, value).apply();
    }

    public static int getInt(String key, int defValue) {
        return sSettingsPre.getInt(key, defValue);
    }

    public static void putInt(String key, int value) {
        sSettingsPre.edit().putInt(key, value).apply();
    }

    public static long getLong(String key, long defValue) {
        return sSettingsPre.getLong(key, defValue);
    }

    public static void putLong(String key, long value) {
        sSettingsPre.edit().putLong(key, value).apply();
    }

    public static float getFloat(String key, float defValue) {
        return sSettingsPre.getFloat(key, defValue);
    }

    public static void putFloat(String key, float value) {
        sSettingsPre.edit().putFloat(key, value).apply();
    }

    public static String getString(String key, String defValue) {
        return sSettingsPre.getString(key, defValue);
    }

    public static void putString(String key, String value) {
        sSettingsPre.edit().putString(key, value).apply();
    }

    public static Set<String> getStringSet(String key, Set<String> defValue) {
        return sSettingsPre.getStringSet(key, defValue);
    }

    public static void putStringSet(String key, Set<String> value) {
        sSettingsPre.edit().remove(key).apply(); // See: https://bon-app-etit.blogspot.com/2013/08/android-bug-stringset-in.html
        sSettingsPre.edit().putStringSet(key, value).apply();
    }

    public static int getIntFromStr(String key, int defValue) {
        return NumberUtils.parseIntSafely(sSettingsPre.getString(key, Integer.toString(defValue)), defValue);
    }

    public static void putIntToStr(String key, int value) {
        sSettingsPre.edit().putString(key, Integer.toString(value)).apply();
    }

    public static final int IMAGE_LOADING_STRATEGY_ALL = 0;
    public static final int IMAGE_LOADING_STRATEGY_WIFI = 1;
    public static final int IMAGE_LOADING_STRATEGY_NO = 2;

    public static final String KEY_DARK_THEME = "dark_theme";
    public static final boolean DEFAULT_DARK_THEME = false;

    public static final String KEY_PRETTY_TIME = "pretty_time";
    public static final boolean DEFAULT_PRETTY_TIME = true;

    public static final String KEY_FONT_SIZE = "font_size";
    public static final int DEFAULT_FONT_SIZE = 16;

    public static final String KEY_LINE_SPACING = "line_spacing";
    public static final int DEFAULT_LINE_SPACING = 1;

    public static final String KEY_DYNAMIC_COMMENTS = "dynamic_comments";
    public static final boolean DEFAULT_DYNAMIC_COMMENTS = true;

    public static final String KEY_FAST_SCROLLER = "fast_scroller";
    public static final boolean DEFAULT_FAST_SCROLLER = true;

    public static final String KEY_COLOR_STATUS_BAR = "color_status_bar";
    public static final boolean DEFAULT_COLOR_STATUS_BAR = true;

    public static final String KEY_FIX_EMOJI_DISPLAY = "fix_emoji_display";
    public static final boolean DEFAULT_FIX_EMOJI_DISPLAY = false;

    public static final String KEY_WATERMARK = "watermark";
    public static final boolean DEFAULT_WATERMARKD = true;

    public static final String KEY_FEED_ID = "feed_id";
    public static final String DEFAULT_FEED_ID = null;

    public static final String KEY_SAVE_IMAGE_AUTO = "save_image_auto";
    public static final boolean DEFAULT_SAVE_IMAGE_AUTO = false;

    public static final String KEY_IMAGE_LOADING_STRATEGY = "image_loading_strategy";
    public static final int DEFAULT_IMAGE_LOADING_STRATEGY = 0;
    public static final String KEY_IMAGE_SAVE_LOACTION = "image_save_location";

    public static final String KEY_IMAGE_LOADING_STRATEGY_2 = "image_loading_strategy_2";
    public static final boolean DEFAULT_IMAGE_LOADING_STRATEGY_2 = false;

    public static final String KEY_IMAGE_SAVE_SCHEME = "image_scheme";
    public static final String KEY_IMAGE_SAVE_AUTHORITY = "image_authority";
    public static final String KEY_IMAGE_SAVE_PATH = "image_path";
    public static final String KEY_IMAGE_SAVE_QUERY = "image_query";
    public static final String KEY_IMAGE_SAVE_FRAGMENT = "image_fragment";

    public static final String KEY_CHAOS_LEVEL = "chaos_level";
    public static final int DEFAULT_CHAOS_LEVEL = 0;

    public static final String KEY_SET_ANALYSIS = "set_analysis";
    public static final boolean DEFAULT_SET_ANALYSIS = false;
    public static final String KEY_ANALYSIS = "analysis";
    public static final boolean DEFAULT_ANALYSIS = false;

    public static final String KEY_FORUM_AUTO_SORTING = "forum_auto_sorting";
    public static final boolean DEFAULT_FORUM_AUTO_SORTING = false;

    public static final String KEY_LAST_FORUM_AGING = "last_forum_aging";
    public static final long DEFAULT_LAST_FORUM_AGING = 0;

    public static boolean getDarkTheme() {
        return getBoolean(KEY_DARK_THEME, DEFAULT_DARK_THEME);
    }

    public static void putDarkTheme(boolean value) {
        putBoolean(KEY_DARK_THEME, value);
    }

    public static boolean getPrettyTime() {
        return getBoolean(KEY_PRETTY_TIME, DEFAULT_PRETTY_TIME);
    }

    public static int getFontSize() {
        return getInt(KEY_FONT_SIZE, DEFAULT_FONT_SIZE);
    }

    public static void putFontSize(int value) {
        putInt(KEY_FONT_SIZE, value);
    }

    public static int getLineSpacing() {
        return getInt(KEY_LINE_SPACING, DEFAULT_LINE_SPACING);
    }

    public static void putLineSpacing(int value) {
        putInt(KEY_LINE_SPACING, value);
    }

    public static boolean getDynamicComments() {
        return getBoolean(KEY_DYNAMIC_COMMENTS, DEFAULT_DYNAMIC_COMMENTS);
    }

    public static boolean getFastScroller() {
        return getBoolean(KEY_FAST_SCROLLER, DEFAULT_FAST_SCROLLER);
    }

    public static boolean getColorStatusBar() {
        return getBoolean(KEY_COLOR_STATUS_BAR, DEFAULT_COLOR_STATUS_BAR);
    }

    public static boolean getFixEmojiDisplay() {
        return getBoolean(KEY_FIX_EMOJI_DISPLAY, DEFAULT_FIX_EMOJI_DISPLAY);
    }

    public static boolean getWatermark() {
        return getBoolean(KEY_WATERMARK, DEFAULT_WATERMARKD);
    }

    public static String getFeedId() {
        if (!sCheckExtendFeedId) {
            sCheckExtendFeedId = true;
            // Get extend feed id
            File feedIdFile = NMBAppConfig.getFileInAppDir(KEY_FEED_ID);
            if (feedIdFile != null) {
                FileInputStream fis;
                try {
                    fis = new FileInputStream(feedIdFile);
                    String feedId = IOUtils.readString(fis, "UTF-8");
                    if (!TextUtils.isEmpty(feedId)) {
                        sExtendFeedId = feedId;
                        putFeedId(sExtendFeedId);
                    }
                } catch (IOException e) {
                    // Ignore
                }
            }
        }

        if (sExtendFeedId != null) {
            return sExtendFeedId;
        } else {
            String feedId = getString(KEY_FEED_ID, DEFAULT_FEED_ID);
            if (!TextUtils.isEmpty(feedId)) {
                return feedId;
            } else {
                feedId = getRandomFeedId();
                putFeedId(feedId);
                return feedId;
            }
        }
    }

    public static void putFeedId(String value) {
        putString(KEY_FEED_ID, value);

        sExtendFeedId = value;

        // Put it to file
        File file = NMBAppConfig.getFileInAppDir(KEY_FEED_ID);
        if (file != null) {
            FileOutputStream os = null;
            try {
                os = new FileOutputStream(file);
                IOUtils.copy(new ByteArrayInputStream(value.getBytes()), os);
                os.flush();
            } catch (IOException e) {
                file.delete();
            } finally {
                IOUtils.closeQuietly(os);
            }
        }
    }

    public static @NonNull String getRandomFeedId() {
        int length = 20;
        StringBuilder sb = new StringBuilder(length);

        for (int i = 0; i < length; i++) {
            if (MathUtils.random(0, 1 + 1) == 0) {
                sb.append((char) MathUtils.random('a', 'z' + 1));
            } else {
                sb.append((char) MathUtils.random('0', '9' + 1));
            }
        }

        return sb.toString();
    }

    public static String getMacFeedId() {
        WifiManager wifi = (WifiManager) sContext.getSystemService(Context.WIFI_SERVICE);
        WifiInfo info = wifi.getConnectionInfo();
        if (info == null) {
            return null;
        }

        String mac = info.getMacAddress();
        if (mac == null) {
            return null;
        }

        String id;
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");
            digest.update(mac.getBytes());
            id = bytesToHexString(digest.digest());
        } catch (NoSuchAlgorithmException e) {
            id = String.valueOf(mac.hashCode());
        }

        return id;
    }

    public static boolean getSaveImageAuto() {
        return getBoolean(KEY_SAVE_IMAGE_AUTO, DEFAULT_SAVE_IMAGE_AUTO);
    }

    public static int getImageLoadingStrategy() {
        return getIntFromStr(KEY_IMAGE_LOADING_STRATEGY, DEFAULT_IMAGE_LOADING_STRATEGY);
    }

    public static boolean getImageLoadingStrategy2() {
        return getBoolean(KEY_IMAGE_LOADING_STRATEGY_2, DEFAULT_IMAGE_LOADING_STRATEGY_2);
    }

    @Nullable
    public static UniFile getImageSaveLocation() {
        UniFile dir = null;
        try {
            Uri.Builder builder = new Uri.Builder();
            builder.scheme(getString(KEY_IMAGE_SAVE_SCHEME, null));
            builder.encodedAuthority(getString(KEY_IMAGE_SAVE_AUTHORITY, null));
            builder.encodedPath(getString(KEY_IMAGE_SAVE_PATH, null));
            builder.encodedQuery(getString(KEY_IMAGE_SAVE_QUERY, null));
            builder.encodedFragment(getString(KEY_IMAGE_SAVE_FRAGMENT, null));
            dir = UniFile.fromUri(sContext, builder.build());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dir != null ? dir : UniFile.fromFile(NMBAppConfig.getImageDir());
    }

    public static void putImageSaveLocation(@NonNull UniFile location) {
        Uri uri = location.getUri();
        putString(KEY_IMAGE_SAVE_SCHEME, uri.getScheme());
        putString(KEY_IMAGE_SAVE_AUTHORITY, uri.getEncodedAuthority());
        putString(KEY_IMAGE_SAVE_PATH, uri.getEncodedPath());
        putString(KEY_IMAGE_SAVE_QUERY, uri.getEncodedQuery());
        putString(KEY_IMAGE_SAVE_FRAGMENT, uri.getEncodedFragment());
    }

    public static int getChaosLevel() {
        return getIntFromStr(KEY_CHAOS_LEVEL, DEFAULT_CHAOS_LEVEL);
    }

    public static boolean getSetAnalysis() {
        return getBoolean(KEY_SET_ANALYSIS, DEFAULT_SET_ANALYSIS);
    }

    public static void putSetAnalysis(boolean value) {
        putBoolean(KEY_SET_ANALYSIS, value);
    }

    public static boolean getAnalysis() {
        return getBoolean(KEY_ANALYSIS, DEFAULT_ANALYSIS);
    }

    public static void putAnalysis(boolean value) {
        putBoolean(KEY_ANALYSIS, value);
    }

    public static boolean getForumAutoSorting() {
        return getBoolean(KEY_FORUM_AUTO_SORTING, DEFAULT_FORUM_AUTO_SORTING);
    }

    public static void putForumAutoSorting(boolean value) {
        putBoolean(KEY_FORUM_AUTO_SORTING, value);
    }

    public static long getLastForumAging() {
        return getLong(KEY_LAST_FORUM_AGING, DEFAULT_LAST_FORUM_AGING);
    }

    public static void setLastForumAging(long value) {
        putLong(KEY_LAST_FORUM_AGING, value);
    }

    private static final String KEY_VERSION_CODE = "version_code";
    private static final int DEFAULT_VERSION_CODE = 0;

    public static int getVersionCode() {
        return getInt(KEY_VERSION_CODE, DEFAULT_VERSION_CODE);
    }

    public static void putVersionCode(int value) {
        putInt(KEY_VERSION_CODE, value);
    }

    public static final String KEY_GUIDE_LIST_ACTIVITY = "guide_list_activity";
    public static final boolean VALUE_GUIDE_LIST_ACTIVITY = true;

    public static boolean getGuideListActivity() {
        return getBoolean(KEY_GUIDE_LIST_ACTIVITY, VALUE_GUIDE_LIST_ACTIVITY);
    }

    public static void putGuideListActivity(boolean value) {
        putBoolean(KEY_GUIDE_LIST_ACTIVITY, value);
    }

    public static final String KEY_NEW_ICON = "new_icon";
    public static final boolean VALUE_NEW_ICON = true;

    public static boolean getNewIcon() {
        return getBoolean(KEY_NEW_ICON, VALUE_NEW_ICON);
    }

    public static void putNewIcon(boolean value) {
        putBoolean(KEY_NEW_ICON, value);
    }

    public static final String[] ICON_ACTIVITY_ARRAY = {
            "com.hippo.nimingban.ui.ListActivity",
            "com.hippo.nimingban.ui.ListActivityIbuki",
    };

    public static String getDefaultIconActivity() {
        return ICON_ACTIVITY_ARRAY[0];
    }

    public static String getCurrentIconActivity() {
        return ICON_ACTIVITY_ARRAY[getIntFromStr("app_icon", 0)];
    }

    public static final String KEY_GUIDE_SORT_FORUMS_ACTIVITY = "guide_sort_forums_activity";
    public static final boolean VALUE_GUIDE_SORT_FORUMS_ACTIVITY = true;

    public static boolean getGuideSortForumsActivity() {
        return getBoolean(KEY_GUIDE_SORT_FORUMS_ACTIVITY, VALUE_GUIDE_SORT_FORUMS_ACTIVITY);
    }

    public static void putGuideSortForumsActivity(boolean value) {
        putBoolean(KEY_GUIDE_SORT_FORUMS_ACTIVITY, value);
    }

    public static final String KEY_GUIDE_SORTING_FOUR_BARS = "guide_sorting_four_bars";
    public static final boolean VALUE_GUIDE_SORTING_FOUR_BARS = true;

    public static boolean getGuideSortingFourBars() {
        return getBoolean(KEY_GUIDE_SORTING_FOUR_BARS, VALUE_GUIDE_SORTING_FOUR_BARS);
    }

    public static void putGuideSortingFourBars(boolean value) {
        putBoolean(KEY_GUIDE_SORTING_FOUR_BARS, value);
    }

    public static final String KEY_GUIDE_PINNING_STAR = "guide_pinning_star";
    public static final boolean VALUE_GUIDE_PINNING_STAR = true;

    public static boolean getGuidePinningStar() {
        return getBoolean(KEY_GUIDE_PINNING_STAR, VALUE_GUIDE_PINNING_STAR);
    }

    public static void putGuidePinningStar(boolean value) {
        putBoolean(KEY_GUIDE_PINNING_STAR, value);
    }

    public static final String KEY_GUIDE_TYPE_SEND = "guide_type_send";
    public static final boolean VALUE_GUIDE_TYPE_SEND = true;

    public static boolean getGuideTypeSend() {
        return getBoolean(KEY_GUIDE_TYPE_SEND, VALUE_GUIDE_TYPE_SEND);
    }

    public static void putGuideTypeSend(boolean value) {
        putBoolean(KEY_GUIDE_TYPE_SEND, value);
    }

    public static final String KEY_NOTICE_DATE = "notice_date";
    public static final long VALUE_NOTICE_DATE = -1;

    public static long getNoticeDate() {
        return getLong(KEY_NOTICE_DATE, VALUE_NOTICE_DATE);
    }

    public static void putNoticeDate(long value) {
        putLong(KEY_NOTICE_DATE, value);
    }

    public static final String KEY_CRASH_FILENAME = "crash_filename";
    public static final String VALUE_CRASH_FILENAME = null;

    public static String getCrashFilename() {
        return getString(KEY_CRASH_FILENAME, VALUE_CRASH_FILENAME);
    }

    @SuppressLint("CommitPrefEdits")
    public static void putCrashFilename(String value) {
        sSettingsPre.edit().putString(KEY_CRASH_FILENAME, value).commit();
    }

    public static final String KEY_AC_HOST = "ac_host";
    public static final String VALUE_AC_HOST = "https://adnmb1.com";

    public static String getAcHost() {
        return getString(KEY_AC_HOST, VALUE_AC_HOST);
    }

    public static void putAcHost(String value) {
        putString(KEY_AC_HOST, value);
    }

    public static final String KEY_ENABLE_CUSTOMIZED_AC_HOST = "enable_customized_ac_host";
    public static final boolean VALUE_ENABLE_CUSTOMIZED_AC_HOST = false;

    public static boolean getEnableCustomizedAcHost() {
        return getBoolean(KEY_ENABLE_CUSTOMIZED_AC_HOST, VALUE_ENABLE_CUSTOMIZED_AC_HOST);
    }

    public static final String KEY_CUSTOMIZED_AC_HOST = "customized_ac_host";
    public static final String VALUE_CUSTOMIZED_AC_HOST = "https://adnmb1.com";

    public static String getCustomizedAcHost() {
        return getString(KEY_CUSTOMIZED_AC_HOST, VALUE_CUSTOMIZED_AC_HOST);
    }

    public static void putCustomizedAcHost(String value) {
        putString(KEY_CUSTOMIZED_AC_HOST, value);
    }

    public static final String KEY_STRICT_IGNORE_MODE = "strict_ignore_post_mode";
    public static final boolean VALUE_STRICT_IGNORE_MODE = false;

    public static boolean getEnableStrictIgnoreMode() {
        return getBoolean(KEY_STRICT_IGNORE_MODE, VALUE_STRICT_IGNORE_MODE);
    }

    /**
     * http://stackoverflow.com/questions/332079
     *
     * @param bytes The bytes to convert.
     * @return A {@link String} converted from the bytes of a hashable key used
     *         to store a filename on the disk, to hex digits.
     */
    private static String bytesToHexString(final byte[] bytes) {
        final StringBuilder builder = new StringBuilder();
        for (final byte b : bytes) {
            final String hex = Integer.toHexString(0xFF & b);
            if (hex.length() == 1) {
                builder.append('0');
            }
            builder.append(hex);
        }
        return builder.toString();
    }
} public class ContentLayout extends FrameLayout {

    private static final String TAG = ContentLayout.class.getSimpleName();

    private ProgressView mProgressView;
    private ViewGroup mTipView;
    private ViewGroup mContentView;

    private RefreshLayout mRefreshLayout;
    private EasyRecyclerView mRecyclerView;
    private FastScroller mFastScroller;
    private View mImageView;
    private TextView mTextView;

    private int mRecyclerViewOriginTop;
    private int mRecyclerViewOriginBottom;

    public ContentLayout(Context context) {
        super(context);
        init(context);
    }

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

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

    private void init(Context context) {
        LayoutInflater.from(context).inflate(R.layout.widget_content_layout, this);

        mProgressView = (ProgressView) findViewById(R.id.progress);
        mTipView = (ViewGroup) findViewById(R.id.tip);
        mContentView = (ViewGroup) findViewById(R.id.content_view);

        mRefreshLayout = (RefreshLayout) mContentView.findViewById(R.id.refresh_layout);
        mFastScroller = (FastScroller) mContentView.findViewById(R.id.fast_scroller);
        mRecyclerView = (EasyRecyclerView) mRefreshLayout.findViewById(R.id.recycler_view);
        mImageView = mTipView.getChildAt(0);
        mTextView = (TextView) mTipView.getChildAt(1);

        mFastScroller.attachToRecyclerView(mRecyclerView);
        HandlerDrawable drawable = new HandlerDrawable();
        drawable.setColor(ResourcesUtils.getAttrColor(context, R.attr.colorAccent));
        mFastScroller.setHandlerDrawable(drawable);

        mRefreshLayout.setHeaderColorSchemeResources(
                R.color.loading_indicator_red,
                R.color.loading_indicator_purple,
                R.color.loading_indicator_blue,
                R.color.loading_indicator_cyan,
                R.color.loading_indicator_green,
                R.color.loading_indicator_yellow);
        mRefreshLayout.setFooterColorSchemeResources(
                R.color.loading_indicator_red,
                R.color.loading_indicator_blue,
                R.color.loading_indicator_green,
                R.color.loading_indicator_orange);
        mRefreshLayout.setHeaderProgressBackgroundColorSchemeColor(ResourcesUtils.getAttrColor(context, R.attr.colorPure));

        mRecyclerViewOriginTop = mRecyclerView.getPaddingTop();
        mRecyclerViewOriginBottom = mRecyclerView.getPaddingBottom();
    }

    public EasyRecyclerView getRecyclerView() {
        return mRecyclerView;
    }

    public void setHelper(ContentHelper helper) {
        helper.init(this);
    }

    public void showFastScroll() {
        if (!mFastScroller.isAttached()) {
            mFastScroller.attachToRecyclerView(mRecyclerView);
        }
    }

    public void hideFastScroll() {
        mFastScroller.detachedFromRecyclerView();
    }

    public void setFitPaddingTop(int fitPaddingTop) {
        // RecyclerView
        mRecyclerView.setPadding(mRecyclerView.getPaddingLeft(), mRecyclerViewOriginTop + fitPaddingTop, mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingBottom());
        // RefreshLayout
        mRefreshLayout.setProgressViewOffset(false, fitPaddingTop, fitPaddingTop + LayoutUtils.dp2pix(getContext(), 32)); // TODO
    }

    public void setFitPaddingBottom(int fitPaddingBottom) {
        // RecyclerView
        mRecyclerView.setPadding(mRecyclerView.getPaddingLeft(),
                mRecyclerView.getPaddingTop(), mRecyclerView.getPaddingRight(),
                mRecyclerViewOriginBottom + fitPaddingBottom);
    }

    public abstract static class ContentHelper<E> {

        private static final String TAG = ContentHelper.class.getSimpleName();

        public static final int TYPE_REFRESH = 0;
        public static final int TYPE_PRE_PAGE = 1;
        public static final int TYPE_PRE_PAGE_KEEP_POS = 2;
        public static final int TYPE_NEXT_PAGE = 3;
        public static final int TYPE_NEXT_PAGE_KEEP_POS = 4;
        public static final int TYPE_SOMEWHERE = 5;
        public static final int TYPE_REFRESH_PAGE = 6;

        public static final int REFRESH_TYPE_HEADER = 0;
        public static final int REFRESH_TYPE_FOOTER = 1;
        public static final int REFRESH_TYPE_PROGRESS_VIEW = 2;

        private ProgressView mProgressView;
        private ViewGroup mTipView;
        private ViewGroup mContentView;

        private RefreshLayout mRefreshLayout;
        private EasyRecyclerView mRecyclerView;
        private View mImageView;
        private TextView mTextView;

        private ViewTransition mViewTransition;

        /**
         * Store data
         */
        private List<E> mData = new ArrayList<>();

        /**
         * Generate task id
         */
        private IntIdGenerator mIdGenerator = new IntIdGenerator();

        /**
         * Store the page divider index
         *
         * For example, the data contain page 3, page 4, page 5,
         * page 3 size is 7, page 4 size is 8, page 5 size is 9,
         * so <code>mPageDivider</code> contain 7, 15, 24.
         */
        private IntList mPageDivider = new IntList();

        /**
         * The first page in <code>mData</code>
         */
        private int mStartPage;

        /**
         * The last page + 1 in <code>mData</code>
         */
        private int mEndPage;

        /**
         * The available page count.
         */
        private int mPages;

        private int mCurrentTaskId;
        private int mCurrentTaskType;
        private int mCurrentTaskPage;

        private int mNextPageScrollSize;

        private String mEmptyString = "No hint";

        private RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                if (!mRefreshLayout.isRefreshing() && mRefreshLayout.isAlmostBottom() && mEndPage < mPages) {
                    // Get next page
                    mRefreshLayout.setFooterRefreshing(true);
                    mOnRefreshListener.onFooterRefresh();
                }
            }
        };

        private RefreshLayout.OnRefreshListener mOnRefreshListener = new RefreshLayout.OnRefreshListener() {
            @Override
            public void onHeaderRefresh() {
                if (mStartPage > 0) {
                    mCurrentTaskId = mIdGenerator.nextId();
                    mCurrentTaskType = TYPE_PRE_PAGE_KEEP_POS;
                    mCurrentTaskPage = mStartPage - 1;
                    getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
                } else {
                    doRefresh();
                }
            }

            @Override
            public void onFooterRefresh() {
                if (mEndPage < mPages) {
                    // Get next page
                    mCurrentTaskId = mIdGenerator.nextId();
                    mCurrentTaskType = TYPE_NEXT_PAGE_KEEP_POS;
                    mCurrentTaskPage = mEndPage;
                    getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
                } else if (mEndPage == mPages) {
                    // Refresh last page
                    mCurrentTaskId = mIdGenerator.nextId();
                    mCurrentTaskType = TYPE_REFRESH_PAGE;
                    mCurrentTaskPage = mEndPage - 1;
                    getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
                } else {
                    Log.e(TAG, "Try to footer refresh, but mEndPage = " + mEndPage + ", mPages = " + mPages);
                }
            }
        };

        private final LayoutManagerUtils.OnScrollToPositionListener mOnScrollToPositionListener =
                new LayoutManagerUtils.OnScrollToPositionListener() {
                    @Override
                    public void onScrollToPosition(int position) {
                        ContentHelper.this.onScrollToPosition();
                    }
                };

        private void init(ContentLayout contentLayout) {
            mNextPageScrollSize = LayoutUtils.dp2pix(contentLayout.getContext(), 48);

            mProgressView = contentLayout.mProgressView;
            mTipView = contentLayout.mTipView;
            mContentView = contentLayout.mContentView;

            mRefreshLayout = contentLayout.mRefreshLayout;
            mRecyclerView = contentLayout.mRecyclerView;
            mImageView = contentLayout.mImageView;
            mTextView = contentLayout.mTextView;

            mViewTransition = new ViewTransition(mContentView, mProgressView, mTipView);
            mViewTransition.showView(2, false);

            mRecyclerView.addOnScrollListener(mOnScrollListener);
            mRefreshLayout.setOnRefreshListener(mOnRefreshListener);

            mTipView.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    refresh();
                }
            });
        }


        /**
         * Call {@link #onGetPageData(int, List)} when get data
         *
         * @param taskId task id
         * @param page the page to get
         */
        protected abstract void getPageData(int taskId, int type, int page);

        protected abstract Context getContext();

        protected abstract void notifyDataSetChanged();

        protected abstract void notifyItemRangeRemoved(int positionStart, int itemCount);

        protected abstract void notifyItemRangeInserted(int positionStart, int itemCount);

        protected void onScrollToPosition() {
        }

        protected void onShowProgress() {
        }

        protected void onShowText() {
        }

        public void setEnable(boolean enable) {
            mRefreshLayout.setEnabled(enable);
        }

        public void setEmptyString(String str) {
            mEmptyString = str;
        }

        /**
         * @throws IndexOutOfBoundsException
         *                if {@code location < 0 || location >= size()}
         */
        public E getDataAt(int location) {
            return mData.get(location);
        }

        public int size() {
            return mData.size();
        }

        public void setPages(int pages) {
            // TODO what it pages > mEndPage
            mPages = pages;
        }

        public int getPages() {
            return mPages;
        }

        public void addAt(int index, E data) {
            mData.add(index, data);

            for (int i = 0, n = mPageDivider.size(); i < n; i++) {
                int divider = mPageDivider.get(i);
                if (index < divider) {
                    mPageDivider.set(i, divider + 1);
                }
            }

            notifyItemRangeInserted(index, 1);
        }

        public void removeAt(int index) {
            mData.remove(index);

            for (int i = 0, n = mPageDivider.size(); i < n; i++) {
                int divider = mPageDivider.get(i);
                if (index < divider) {
                    mPageDivider.set(i, divider - 1);
                }
            }

            notifyItemRangeRemoved(index, 1);
        }

        public void onGetEmptyData(int taskId) {
            if (mCurrentTaskId != taskId) {
                return;
            }

            switch (mCurrentTaskType) {
                case TYPE_REFRESH:
                case TYPE_SOMEWHERE:
                    showText(mEmptyString);
                    break;
                case TYPE_NEXT_PAGE:
                case TYPE_NEXT_PAGE_KEEP_POS:
                    // Last page ?
                    break;
                case TYPE_PRE_PAGE:
                case TYPE_PRE_PAGE_KEEP_POS:
                case TYPE_REFRESH_PAGE:
                    // TODO
                    break;
            }

            mRefreshLayout.setHeaderRefreshing(false);
            mRefreshLayout.setFooterRefreshing(false);
        }

        protected boolean shouldRemoveDuplications() {
            return false;
        }

        protected boolean isTheSame(E d1, E d2) {
            return false;
        }

        private boolean isDuplication(E d) {
            for (E d2 : mData) {
                if (isTheSame(d, d2)) {
                    return true;
                }
            }
            return false;
        }

        private List<E> removeDuplications(List<E> data) {
            if (!shouldRemoveDuplications()) {
                return data;
            }

            List<E> newData = new ArrayList<>(data.size());

            for (E d : data) {
                if (!isDuplication(d)) {
                    newData.add(d);
                }
            }

            // At least keep one item
            if (newData.isEmpty() && !data.isEmpty()) {
                newData.add(data.get(0));
            }

            return newData;
        }

        public void onGetPageData(int taskId, List<E> data) {
            if (mCurrentTaskId == taskId) {
                showContent();

                int dataSize;
                switch (mCurrentTaskType) {
                    case TYPE_REFRESH:
                        mStartPage = 0;
                        mEndPage = 1;
                        mPageDivider.clear();
                        mPageDivider.add(data.size());

                        mData.clear();
                        mData.addAll(data);
                        notifyDataSetChanged();

                        mRecyclerView.stopScroll();
                        LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), 0, 0);
                        onScrollToPosition();
                        break;
                    case TYPE_PRE_PAGE:
                    case TYPE_PRE_PAGE_KEEP_POS:
                        data = removeDuplications(data);
                        mData.addAll(0, data);
                        notifyItemRangeInserted(0, data.size());

                        dataSize = data.size();
                        for (int i = 0, n = mPageDivider.size(); i < n; i++) {
                            mPageDivider.set(i, mPageDivider.get(i) + dataSize);
                        }
                        mPageDivider.add(0, dataSize);

                        mStartPage--;
                        // assert mStartPage >= 0
                        if (mCurrentTaskType == TYPE_PRE_PAGE_KEEP_POS) {
                            mRecyclerView.stopScroll();
                            LayoutManagerUtils.scrollToPositionProperly(mRecyclerView.getLayoutManager(), getContext(),
                                    dataSize - 1, mOnScrollToPositionListener);
                        } else {
                            mRecyclerView.stopScroll();
                            LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), 0, 0);
                            onScrollToPosition();
                        }
                        break;
                    case TYPE_NEXT_PAGE:
                    case TYPE_NEXT_PAGE_KEEP_POS:
                        data = removeDuplications(data);
                        dataSize = data.size();
                        int oldDataSize = mData.size();
                        mData.addAll(data);
                        notifyItemRangeInserted(oldDataSize, dataSize);

                        mPageDivider.add(oldDataSize + dataSize);

                        mEndPage++;
                        if (mCurrentTaskType == TYPE_NEXT_PAGE_KEEP_POS) {
                            mRecyclerView.stopScroll();
                            mRecyclerView.smoothScrollBy(0, mNextPageScrollSize);
                            onScrollToPosition();
                        } else {
                            mRecyclerView.stopScroll();
                            LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), oldDataSize, 0);
                            onScrollToPosition();
                        }
                        break;
                    case TYPE_SOMEWHERE:
                        mData.clear();
                        mData.addAll(data);
                        notifyDataSetChanged();

                        mStartPage = mCurrentTaskPage;
                        mEndPage = mCurrentTaskPage + 1;

                        mPageDivider.clear();
                        mPageDivider.add(data.size());

                        mRecyclerView.stopScroll();
                        LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), 0, 0);
                        onScrollToPosition();
                        break;
                    case TYPE_REFRESH_PAGE:
                        if (mCurrentTaskPage < mStartPage || mCurrentTaskPage >= mEndPage) {
                            Log.e(TAG, "TYPE_REFRESH_PAGE, but mCurrentTaskPage = " + mCurrentTaskPage +
                                    ", mStartPage = " + mStartPage + ", mEndPage = " + mEndPage);
                            break;
                        }

                        int oldIndexStart = mCurrentTaskPage == mStartPage ? 0 : mPageDivider.get(mCurrentTaskPage - mStartPage - 1);
                        int oldIndexEnd = mPageDivider.get(mCurrentTaskPage - mStartPage);
                        mData.subList(oldIndexStart, oldIndexEnd).clear();
                        data = removeDuplications(data);
                        int newIndexStart = oldIndexStart;
                        int newIndexEnd = newIndexStart + data.size();
                        mData.addAll(oldIndexStart, data);
                        notifyDataSetChanged();

                        for (int i = mCurrentTaskPage - mStartPage, n = mPageDivider.size(); i < n; i++) {
                            mPageDivider.set(i, mPageDivider.get(i) - oldIndexEnd + newIndexEnd);
                        }

                        if (newIndexEnd > oldIndexEnd && newIndexEnd > 0) {
                            mRecyclerView.stopScroll();
                            LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), newIndexEnd - 1, 0);
                            onScrollToPosition();
                        }
                        break;
                }
            }

            mRefreshLayout.setHeaderRefreshing(false);
            mRefreshLayout.setFooterRefreshing(false);
        }

        public void onGetExpection(int taskId, Exception e) {
            if (mCurrentTaskId == taskId) {
                if (e != null) {
                    e.printStackTrace();
                }

                mRefreshLayout.setHeaderRefreshing(false);
                mRefreshLayout.setFooterRefreshing(false);
                Say.d(TAG, "Get page data failed " + e.getClass().getName() + " " + e.getMessage());
                String readableError = ExceptionUtils.getReadableString(getContext(), e);
                String reason = ExceptionUtils.getReasonString(getContext(), e);
                if (reason != null) {
                    readableError += '\n' + reason;
                }
                if (mViewTransition.getShownViewIndex() == 0) {
                    Toast.makeText(getContext(), readableError, Toast.LENGTH_SHORT).show();
                } else {
                    showText(readableError);
                }
            }
        }

        public void showContent() {
            mViewTransition.showView(0);
        }

        private boolean isContentShowning() {
            return mViewTransition.getShownViewIndex() == 0;
        }

        public void showProgressBar() {
            mViewTransition.showView(1, false);
        }

        public void showProgressBar(boolean animation) {
            if (mViewTransition.showView(1, animation)) {
                onShowProgress();
            }
        }

        public void showText(CharSequence text) {
            mTextView.setText(text);
            if (mViewTransition.showView(2)) {
                onShowText();
            }
        }

        public void showEmptyString() {
            showText(mEmptyString);
        }

        /**
         * Be carefull
         */
        public void doGetData(int type, int page, int refreshType) {
            switch (refreshType) {
                default:
                case REFRESH_TYPE_HEADER:
                    showContent();
                    mRefreshLayout.setFooterRefreshing(false);
                    mRefreshLayout.setHeaderRefreshing(true);
                    break;
                case REFRESH_TYPE_FOOTER:
                    showContent();
                    mRefreshLayout.setHeaderRefreshing(false);
                    mRefreshLayout.setFooterRefreshing(true);
                    break;
                case REFRESH_TYPE_PROGRESS_VIEW:
                    showProgressBar();
                    mRefreshLayout.setHeaderRefreshing(false);
                    mRefreshLayout.setFooterRefreshing(false);
                    break;
            }

            mCurrentTaskId = mIdGenerator.nextId();
            mCurrentTaskType = type;
            mCurrentTaskPage = page;
            getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
        }

        private void doRefresh() {
            mCurrentTaskId = mIdGenerator.nextId();
            mCurrentTaskType = TYPE_REFRESH;
            mCurrentTaskPage = 0;
            getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
        }

        /**
         * Lisk {@link #refresh()}, but no animation when show progress bar
         */
        public void firstRefresh() {
            showProgressBar(false);
            doRefresh();
        }

        /**
         * Show progress bar first, than do refresh
         */
        public void refresh() {
            showProgressBar();
            doRefresh();
        }

        private void cancelCurrentTask() {
            mCurrentTaskId = mIdGenerator.nextId();
            mRefreshLayout.setHeaderRefreshing(false);
            mRefreshLayout.setFooterRefreshing(false);
        }

        private int getPageStart(int page) {
            if (mStartPage == page) {
                return 0;
            } else {
                return mPageDivider.get(page - mStartPage - 1);
            }
        }

        private int getPageEnd(int page) {
            return mPageDivider.get(page - mStartPage);
        }

        private int getPageForPosition(int position) {
            if (position < 0) {
                return -1;
            }

            IntList pageDivider = mPageDivider;
            for (int i = 0, n = pageDivider.size(); i < n; i++) {
                if (position < pageDivider.get(i)) {
                    return i + mStartPage;
                }
            }

            return -1;
        }

        public int getPageForTop() {
            return getPageForPosition(LayoutManagerUtils.getFirstVisibleItemPosition(mRecyclerView.getLayoutManager()));
        }

        public int getPageForBottom() {
            return getPageForPosition(LayoutManagerUtils.getLastVisibleItemPosition(mRecyclerView.getLayoutManager()));
        }

        public boolean canGoTo() {
            return isContentShowning();
        }

        /**
         * Check range first!
         *
         * @param page the targe page
         * @throws IndexOutOfBoundsException
         */
        public void goTo(int page) throws IndexOutOfBoundsException {
            if (page < 0 || page >= mPages) {
                throw new IndexOutOfBoundsException("Page count is " + mPages + ", page is " + page);
            } else if (page >= mStartPage && page < mEndPage) {
                cancelCurrentTask();

                int position = getPageStart(page);
                mRecyclerView.stopScroll();
                LayoutManagerUtils.scrollToPositionWithOffset(mRecyclerView.getLayoutManager(), position, 0);
                onScrollToPosition();
            } else if (page == mStartPage - 1) {
                mRefreshLayout.setFooterRefreshing(false);
                mRefreshLayout.setHeaderRefreshing(true);

                mCurrentTaskId = mIdGenerator.nextId();
                mCurrentTaskType = TYPE_PRE_PAGE;
                mCurrentTaskPage = page;
                getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
            } else if (page == mEndPage) {
                mRefreshLayout.setHeaderRefreshing(false);
                mRefreshLayout.setFooterRefreshing(true);

                mCurrentTaskId = mIdGenerator.nextId();
                mCurrentTaskType = TYPE_NEXT_PAGE;
                mCurrentTaskPage = page;
                getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
            } else {
                mRefreshLayout.setFooterRefreshing(false);
                mRefreshLayout.setHeaderRefreshing(true);

                mCurrentTaskId = mIdGenerator.nextId();
                mCurrentTaskType = TYPE_SOMEWHERE;
                mCurrentTaskPage = page;
                getPageData(mCurrentTaskId, mCurrentTaskType, mCurrentTaskPage);
            }
        }
    }
} public class FontTextView extends TextView {

    static Typeface sTypeface;

    private Typeface mOriginalTypeface;

    public FontTextView(Context context) {
        super(context);
        init(context);
    }

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

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

    private void ensureTypeface(Context context) {
        if (sTypeface == null) {
            sTypeface = Typeface.createFromAsset(context.getAssets(), "missing_characters.ttf");
        }
    }

    private void init(Context context) {
        mOriginalTypeface = getTypeface();
        ensureTypeface(context);

        if (Settings.getFixEmojiDisplay()) {
            useCustomTypeface();
        }
    }

    public void useCustomTypeface() {
        setTypeface(sTypeface);
    }

    public void useOriginalTypeface() {
        setTypeface(mOriginalTypeface);
    }
}
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem;
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder;
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
import com.hippo.easyrecyclerview.EasyRecyclerView;
import com.hippo.easyrecyclerview.MarginItemDecoration;
import com.hippo.easyrecyclerview.RawMarginItemDecoration;
import com.hippo.nimingban.Constants;
import com.hippo.nimingban.NMBApplication;
import com.hippo.nimingban.R;
import com.hippo.nimingban.client.NMBClient;
import com.hippo.nimingban.client.NMBRequest;
import com.hippo.nimingban.client.ac.data.ACItemUtils;
import com.hippo.nimingban.client.data.ACSite;
import com.hippo.nimingban.client.data.Post;
import com.hippo.nimingban.client.data.Site;
import com.hippo.nimingban.util.ReadableTime;
import com.hippo.nimingban.util.Settings;
import com.hippo.nimingban.widget.ContentLayout;
import com.hippo.nimingban.widget.FontTextView;
import com.hippo.nimingban.widget.LoadImageView;
import com.hippo.ripple.Ripple;
import com.hippo.util.DrawableManager;
import com.hippo.widget.Snackbar;
import com.hippo.yorozuya.LayoutUtils;
import com.hippo.yorozuya.Messenger;
import com.hippo.yorozuya.NumberUtils;
import com.hippo.yorozuya.ObjectUtils;
import com.hippo.yorozuya.ResourcesUtils;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import jp.wasabeef.recyclerview.animators.SlideInUpAnimator;
            mRecyclerViewSwipeManager.release();
            mRecyclerViewSwipeManager = null;
        }

        if (mRecyclerViewTouchActionGuardManager != null) {
            mRecyclerViewTouchActionGuardManager.release();
            mRecyclerViewTouchActionGuardManager = null;
        }

        if (mOnScrollListener != null) {
            mRecyclerView.removeOnScrollListener(mOnScrollListener);
            mOnScrollListener = null;
        }

        if (mRecyclerView != null) {
            mRecyclerView.setItemAnimator(null);
            mRecyclerView.setAdapter(null);
            mRecyclerView = null;
        }

        if (mWrappedAdapter != null) {
            WrapperAdapterUtils.releaseAll(mWrappedAdapter);
            mWrappedAdapter = null;
        }
        mAdapter = null;

        if (mNMBRequest != null) {
            mNMBRequest.cancel();
            mNMBRequest = null;
        }

        for (WeakReference<FeedHolder> ref : mHolderList) {
            FeedHolder holder = ref.get();
            if (holder != null) {
                holder.thumb.unload();
            }
        }
        mHolderList.clear();
    }

    private void resumeHolders() {
        Iterator<WeakReference<FeedHolder>> iterator = mHolderList.iterator();
        while (iterator.hasNext()) {
            FeedHolder holder = iterator.next().get();
            if (holder != null) {
                // Only resume attached view holder
                if (holder.itemView.getParent() != null) {
                    holder.thumb.start();
                }
            } else {
                iterator.remove();
            }
        }
    }

    @Override
    public void onResume() {
        super.onResume();
        resumeHolders();
    }

    private void pauseHolders() {
        Iterator<WeakReference<FeedHolder>> iterator = mHolderList.iterator();
        while (iterator.hasNext()) {
            FeedHolder holder = iterator.next().get();
            if (holder != null) {
                holder.thumb.stop();
            } else {
                iterator.remove();
            }
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        pauseHolders();
    }

    @Override
    public void onReceive(int id, Object obj) {
        if (Constants.MESSENGER_ID_FAST_SCROLLER == id) {
            if (obj instanceof Boolean) {
                if ((Boolean) obj) {
                    mContentLayout.showFastScroll();
                } else {
                    mContentLayout.hideFastScroll();
                }
            }
        } else {
            super.onReceive(id, obj);
        }
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                finish();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public boolean onItemClick(EasyRecyclerView parent, View view, int position, long id) {
        Intent intent = new Intent(this, PostActivity.class);
        intent.setAction(PostActivity.ACTION_POST);
        intent.putExtra(PostActivity.KEY_POST, mFeedHelper.getDataAt(position));
        startActivity(intent);
        return true;
    }

    private class FeedHolder extends AbstractSwipeableItemViewHolder implements View.OnClickListener {

        public View swipable;
        public TextView leftText;
        public TextView centerText;
        public TextView rightText;
        private FontTextView content;