public class Aequorea extends Application {
    private static Aequorea mApp;
    private static String mCurrentTheme;
    private static ExecutorService mExecutor;
    
    @Override
    public void onCreate() {
        super.onCreate();
        
        mApp = this;
        mCurrentTheme = ThemeHelper.getTheme();
        mExecutor = Executors.newCachedThreadPool();
    
        initCache();
        initFlurry();
    }

    private void initFlurry() {
        new FlurryAgent.Builder()
                .withDataSaleOptOut(false) //CCPA - the default value is false
                .withCaptureUncaughtExceptions(true)
                .withIncludeBackgroundSessionsInMetrics(true)
                .withLogLevel(Log.VERBOSE)
                .withPerformanceMetrics(FlurryPerformance.ALL)
                .build(this, "CXH6FJ9VS4P8K3K8BFN9");
    }
    
    private void initCache() {
        File cacheDir = new File(getCacheDir().getAbsolutePath() + File.separator + Constants.ARTICLE_PIC_CACHE);
        RichText.initCacheDir(cacheDir);
        ArticleCache.getCache();
    }
    
    public static Application getApp() {
        return mApp;
    }
    
    public static String getCurrentTheme() {
        return mCurrentTheme;
    }
    
    public static boolean isLightTheme() {
        return mCurrentTheme.equals(Constants.THEME_LIGHT);
    }
    
    public static void setCurrentTheme(String theme) {
        mCurrentTheme = theme;
    }
    
    public static ExecutorService getExecutor(){
        return mExecutor;
    }
} public class Constants {
    public static final String AUTHOR = "author";
    public static final String TAG = "tag";
    public static final String ARTICLE_ID = "article_id";
    public static final String AUTHOR_ID = "author_id";
    public static final String TAG_ID = "tag_id";
    public static final String SEARCH_KEY = "search_key";
    public static final String PHOTO = "photo";
    public static final String ARG_URL = "arg_url";
    
    // Article Type
    public static final String ARTICLE_TYPE_MAGAZINE = "magazine_article"; // 封面故事
    public static final String ARTICLE_TYPE_MAGAZINE_V2 = "magazine"; // 封面故事
    public static final String ARTICLE_TYPE_NORMAL = "normal_article"; // 一般文章
    public static final String ARTICLE_TYPE_NORMAL_V2 = "normal"; // 一般文章
    public static final String ARTICLE_TYPE_THEME = "theme"; // 专题
    public static final String ARTICLE_TYPE_TOP_ARTICLE = "top_article"; // 头条故事
    public static final String ARTICLE_TYPE_SUBJECT = "subject"; // 单行本


    public static final int TYPE_MAGAZINE = 1;
    public static final int TYPE_NORMAL = 2;
    public static final int TYPE_THEME = 3;
    public static final int TYPE_TOP_ARTICLE = 4;
    public static final int TYPE_SUBJECT = 5;
    
    // Theme
    public static final String THEME = "theme";
    public static final String THEME_LIGHT = "theme_light";
    public static final String THEME_DARK = "theme_dark";
    
    // Trigger
    public static final int AUTO_LOAD_TRIGGER = 30;
    
    // Key of SharePreference
    public static final String SP_LATEST_MAIN_PAGE = "sp_latest_main_page";
    public static final String SP_HD_SCREENSHOT = "sp_hd_screenshot";
    public static final String SP_ENABLE_SELECTION = "sp_enable_selection";
    public static final String SP_OFFLINE_CACHE = "sp_offline_cache";
    public static final String SP_DISABLE_RECOMMEND_ARTICLE = "sp_disable_recommend_article";
    
    // Cache dir
    public static final String ARTICLE_CACHE = "article_cache";
    public static final String ARTICLE_PIC_CACHE = "article_pic_cache";
} public class CacheService extends Service {
    
    private OkHttpClient mClient;
    private NetworkService mService;
    private CompositeDisposable mComposite;
    private LinkedList<Long> mArticleList;
    private HashSet<Call> mDownloadTasks;
    private DiskLruCache mDiskLruCache;
    
    private NotificationManager mNotificationManager;
    private BroadcastReceiver mNetworkChangeReceiver;
    
    private int mPicToCacheSize;
    private int mPicCachedCount;
    private static final String CHANNEL_ID = "aequorea_cache";
    
    private static final Pattern IMAGE_TAG_PATTERN = Pattern.compile("<(img|IMG)(.*?)>");
    private static final Pattern IMAGE_SRC_PATTERN = Pattern.compile("(src|SRC)=\"(.*?)\"");
    
    private static final String TAG = CacheService.class.getSimpleName();
    
    @Override
    public void onCreate() {
        super.onCreate();
        
        mClient = new OkHttpClient();
        IntentFilter intentFilter = new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION);
        mService = RequestManager.getInstance().getRetrofit().create(NetworkService.class);
        mComposite = new CompositeDisposable();
        
        mNetworkChangeReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (!NetworkUtils.isWiFiNetwork()) {
                    cancelCaching();
                }
            }
        };
        
        registerReceiver(mNetworkChangeReceiver, intentFilter);
        
        Data dataToCache = getMainPageDataFromSP();
        if (dataToCache != null) {
            preCache(dataToCache);
            
            if (mArticleList.size() != 0) {
                showNotification();
                startCaching();
            }
        }
    }
    
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
    
    @Override
    public void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mNetworkChangeReceiver);
        hideNotification();
        if (mComposite != null) {
            mComposite.clear();
        }
    }
    
    @SuppressLint("UseSparseArrays")
    private void preCache(Data data) {
        mArticleList = new LinkedList<>();
        mDownloadTasks = new HashSet<>();
        mDiskLruCache = CacheUtils.getArticlePicDiskLruCache();
        
        List<Datum> dataList = FilterUtils.filterData(data.getData());
        
        int size = dataList.size();
        for (int i = 0; i < size; i++) {
            // check whether this article have been cached in memory or internal storage
            Datum d = dataList.get(i).getData().get(0);
            if (needToCacheArticle(d.getId())) {
                mArticleList.add(d.getId());
            }
        }
    }
    
    private void showNotification() {
        mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, getString(R.string.aequorea_offline_cache), NotificationManager.IMPORTANCE_DEFAULT);
            mNotificationManager.createNotificationChannel(channel);
        }
        
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID).setSmallIcon(R.mipmap.ic_notification)
            .setContentTitle(getString(R.string.app_name))
            .setContentText(getString(R.string.caching_offline_article));
        
        if (mNotificationManager != null) {
            mNotificationManager.notify(1, builder.build());
        }
    }
    
    private void hideNotification() {
        if (mNotificationManager != null) {
            mNotificationManager.cancel(1);
        }
    }
    
    /**
     * Cache only text data, it won't take long.
     */
    private void startCaching() {
        mPicCachedCount = 0;
        mPicToCacheSize = 0;
        
        if (mArticleList.size() == 0) {
            Log.d(TAG, "No more article to cache, stop and leave");
            stopAndLeave();
            return;
        }
        
        long id = mArticleList.poll();
        
        if (needToCacheArticle(id)) {
            cacheArticleText(id);
        }
    }
    
    private void cacheArticleText(final long id) {
        Disposable disposable = mService.getArticleDetailInfo(id)
            .subscribeOn(Schedulers.io())
            .subscribe(new Consumer<DataWrapper>() {
                @Override
                public void accept(DataWrapper article) throws Exception {
                    cacheArticleText(id, article);
                    
                    cacheArticlePic(article.getData().getContent());
                    Log.d(TAG, id + " text cache succeed");
                }
            }, new Consumer<Throwable>() {
                @Override
                public void accept(Throwable throwable) throws Exception {
                    throwable.printStackTrace();
                }
            });
        mComposite.add(disposable);
    }
    
    private void cacheArticleText(long id, DataWrapper article) {
        Gson gson = new Gson();
        String json = gson.toJson(article);
        ArticleCache.getCache().cache(id, json);
    }
    
    /**
     * Cache pic in article and show caching notification.
     */
    private void cacheArticlePic(final String text) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                List<String> picList = analyzeImages(text);
                
                mPicToCacheSize = picList.size();
                
                // this article contains no pic
                if (mPicToCacheSize == 0) {
                    startCaching();
                    return;
                }
                
                for (final String url : picList) {
                    Request builder = new Request.Builder().url(url).build();
                    Call call = mClient.newCall(builder);
                    call.enqueue(new WriteToCacheCallback());
                    mDownloadTasks.add(call);
                }
            }
        };
        
        Aequorea.getExecutor().submit(runnable);
    }
    
    /**
     * Retrieve image src list from text.
     */
    private List<String> analyzeImages(String text) {
        List<String> picList = new ArrayList<>();
        Matcher imageTagMatcher = IMAGE_TAG_PATTERN.matcher(text);
        while (imageTagMatcher.find()) {
            String image = imageTagMatcher.group(2).trim();
            Matcher imageSrcMatcher = IMAGE_SRC_PATTERN.matcher(image);
            String src = null;
            if (imageSrcMatcher.find()) {
                src = imageSrcMatcher.group(2).trim();
            }
            if (!TextUtils.isEmpty(src)) {
                picList.add(src);
            }
        }
        return picList;
    }
    
    private boolean needToCacheArticle(long id) {
        return TextUtils.isEmpty(ArticleCache.getCache().loadCache(id)) && !ArticleCache.getCache()
            .isCachedInStorage(id);
    }
    
    private Data getMainPageDataFromSP() {
        String cacheString = SPUtils.getString(Constants.SP_LATEST_MAIN_PAGE);
        if (!TextUtils.isEmpty(cacheString)) {
            Gson gson = new Gson();
            return gson.fromJson(cacheString, Data.class);
        }
        return null;
    }
    
    private class WriteToCacheCallback implements Callback {
        @Override
        public void onFailure(Call call, IOException e) {
            e.printStackTrace();
            call.cancel();
            
            checkPicCachingProcedure();
        }
        
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            InputStream in = response.body().byteStream();
            
            String key = generateKey(call.request().url().url().toString());
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            
            Log.d(TAG, key + " cached");
            
            if (snapshot == null) {
                DiskLruCache.Editor edit = mDiskLruCache.edit(key);
                
                
                OutputStream os = edit.newOutputStream(0);
                os.write(0);
                
                byte[] buffer = new byte[2048];
                int len;
                OutputStream outputStream = edit.newOutputStream(1);
                outputStream.write(1);
                while ((len = in.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, len);
                }
                outputStream.flush();
                outputStream.close();
                
                edit.commit();
            }
            
            checkPicCachingProcedure();
        }
    }
    
    private void checkPicCachingProcedure() {
        mPicCachedCount++;
        if (mPicCachedCount == mPicToCacheSize) {
            startCaching();
        }
    }
    
    private String generateKey(String url) {
        return MD5.generate(url);
    }
    
    private void cancelCaching() {
        for (Call call : mDownloadTasks) {
            call.cancel();
        }
        stopAndLeave();
    }
    
    private void stopAndLeave() {
        hideNotification();
        stopSelf();
    }
} public abstract class BaseActivity extends AppCompatActivity implements BaseView {
    public String currentTheme;
    public boolean needToReTheme;
    
    private boolean isForeground;
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initTheme();
        
        setContentView(getContentViewId());
        initModel();
        initView();
        initPresenter();
    }
    
    protected void initTheme() {
        currentTheme = ThemeHelper.getTheme();
        setTheme(ThemeHelper.getThemeStyle(currentTheme));
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        
        isForeground = true;
        if (needToReTheme) {
            onThemeSwitchPending();
        }
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        
        isForeground = false;
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
    
    public void setStatusBarStyle(boolean isLightStatusBar) {
        DisplayUtils.setStatusBarStyle(this, isLightStatusBar);
    }
    
    public void setStatusBarInLowProfileMode(boolean isLightStautsBar) {
        DisplayUtils.setStatusInLowProfileMode(this, isLightStautsBar);
    }
    
    public int dp2px(int dp) {
        return DisplayUtils.dp2px(this, dp);
    }
    
    protected abstract int getContentViewId();
    
    protected abstract void initModel();
    
    protected abstract void initView();
    
    protected abstract void initPresenter();
    
    // For those activity that need to change color when user have changed theme.
    @Override
    public void onThemeSwitch() {
        // do nothing
    }
    
    @Override
    public void onThemeSwitchPending() {
        // do nothing
    }
    
    public boolean isInLightTheme() {
        return currentTheme.equals(Constants.THEME_LIGHT);
    }
    
    public boolean activityInForeground() {
        return isForeground;
    }
    
    @Override
    public void onBackPressed() {
        super.onBackPressed();
        overridePendingTransition(0, R.anim.activity_slide_out_bottom);
    }
} public class MaterialSearchView extends FrameLayout implements Filter.FilterListener {
    
    private boolean mIsSearchOpen = false;
    private int mAnimationDuration;
    private boolean mClearingFocus;
    private boolean mSuggestionVisible;
    private String mTheme;
    
    //Views
    private View mSearchLayout;
    private View mDividerView;
    private ListView mSuggestionsListView;
    private EditText mSearchSrcTextView;
    private ImageButton mBackBtn;
    private ImageButton mEmptyBtn;
    private RelativeLayout mSearchTopBar;
    
    private CharSequence mOldQueryText;
    
    private MaterialSearchView.OnQueryTextListener mOnQueryChangeListener;
    private MaterialSearchView.SearchViewListener mSearchViewListener;
    
    private InstantSearchAdapter mAdapter;
    
    private Context mContext;
    
    public MaterialSearchView(Context context) {
        this(context, null);
    }
    
    public MaterialSearchView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }
    
    public MaterialSearchView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs);
        
        mContext = context;
        
        initiateView();
    }
    
    private void initiateView() {
        LayoutInflater.from(mContext).inflate(R.layout.layout_search_view, this, true);
        
        mSearchLayout = findViewById(R.id.search_layout);
        mSearchTopBar = mSearchLayout.findViewById(R.id.search_top_bar);
        mSuggestionsListView = mSearchLayout.findViewById(R.id.suggestion_list);
        mSearchSrcTextView = mSearchLayout.findViewById(R.id.searchTextView);
        mBackBtn = mSearchLayout.findViewById(R.id.action_up_btn);
        mEmptyBtn = mSearchLayout.findViewById(R.id.action_empty_btn);
        mDividerView = mSearchLayout.findViewById(R.id.view_divider);
        
        mSearchSrcTextView.setOnClickListener(mOnClickListener);
        mBackBtn.setOnClickListener(mOnClickListener);
        mEmptyBtn.setOnClickListener(mOnClickListener);
    
        applyTheme();
        initSearchView();
        setAnimationDuration(AnimationUtil.ANIMATION_DURATION_MEDIUM);
    }
    
    private void initSearchView() {
        mSearchSrcTextView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
                onSubmitQuery();
                return true;
            }
        });
        
        mSearchSrcTextView.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // do nothing
            }
            
            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                startFilter(s);
                MaterialSearchView.this.onTextChanged(s);
                if (s.length() == 0 && mSuggestionsListView != null) {
                    if (mAdapter != null) {
                        animateSuggestions(mAdapter.getListHeight(), 0);
                    }
                    mAdapter = new InstantSearchAdapter(getContext(), null);
                    mSuggestionsListView.setAdapter(mAdapter);
                }
            }
            
            @Override
            public void afterTextChanged(Editable s) {
                
            }
        });
        
        mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {
                    showKeyboard(mSearchSrcTextView);
                    showSuggestions();
                }
            }
        });
    }
    
    private void startFilter(CharSequence s) {
        if (mAdapter != null && mAdapter instanceof Filterable) {
            ((Filterable) mAdapter).getFilter().filter(s, MaterialSearchView.this);
        }
    }
    
    private final OnClickListener mOnClickListener = new OnClickListener() {
        
        public void onClick(View v) {
            if (v == mBackBtn) {
                closeSearch();
            } else if (v == mEmptyBtn) {
                mSearchSrcTextView.setText(null);
            } else if (v == mSearchSrcTextView) {
                showSuggestions();
            }
        }
    };
    
    private void onTextChanged(CharSequence newText) {
        CharSequence text = mSearchSrcTextView.getText();
        boolean hasText = !TextUtils.isEmpty(text);
        if (hasText) {
            mEmptyBtn.setVisibility(VISIBLE);
        } else {
            mEmptyBtn.setVisibility(GONE);
        }
        
        if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
            mOnQueryChangeListener.onQueryTextChange(newText.toString());
        }
        mOldQueryText = newText.toString();
    }
    
    private void onSubmitQuery() {
        CharSequence query = mSearchSrcTextView.getText();
        if (query != null && TextUtils.getTrimmedLength(query) > 0) {
            if (mOnQueryChangeListener == null || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
                closeSearch();
                mSearchSrcTextView.setText(null);
            }
        }
    }
    
    public void hideKeyboard(View view) {
        InputMethodManager imm = (InputMethodManager) view.getContext()
            .getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
            imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
        }
    }
    
    public void showKeyboard(View view) {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1 && view.hasFocus()) {
            view.clearFocus();
        }
        view.requestFocus();
        InputMethodManager imm = (InputMethodManager) view.getContext()
            .getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
            imm.showSoftInput(view, 0);
        }
    }
    
    //Public Attributes
    
    @Override
    public void setBackground(Drawable background) {
        mSearchTopBar.setBackground(background);
    }
    
    @Override
    public void setBackgroundColor(int color) {
        mSearchTopBar.setBackgroundColor(color);
    }
    
    public void setTextColor(int color) {
        mSearchSrcTextView.setTextColor(color);
    }
    
    public void setHint(CharSequence hint) {
        mSearchSrcTextView.setHint(hint);
    }
    
    public void showSuggestions() {
        if (mAdapter == null || mAdapter.getCount() == 0) return;
        
        if (!mSuggestionVisible) {
            animateSuggestions(0, mAdapter.getListHeight());
        } else {
            animateSuggestions(mSuggestionsListView.getHeight(), mAdapter.getListHeight());
        }
    }
    
    public void dismissSuggestions() {
        if (mSuggestionVisible) {
            animateSuggestions(mSuggestionsListView.getHeight(), 0);
        }
    }
    
    private void animateSuggestions(int from, int to) {
        mSuggestionVisible = to > 0;
        
        final ViewGroup.LayoutParams lp = mSuggestionsListView.getLayoutParams();
        if (to == 0 && lp.height == 0) return;
        ValueAnimator animator = ValueAnimator.ofInt(from, to);
        animator.setDuration(200);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                lp.height = (int) animation.getAnimatedValue();
                mSuggestionsListView.setLayoutParams(lp);
            }
        });
        animator.start();
    }
    
    /**
     * Set Suggest List OnItemClickListener
     *
     */
    public void setOnItemClickListener(AdapterView.OnItemClickListener listener) {
        mSuggestionsListView.setOnItemClickListener(listener);
    }
    
    /**
     * Set Adapter for suggestions list. Should implement Filterable.
     *
     */
    public void setAdapter(InstantSearchAdapter adapter) {
        mAdapter = adapter;
        mSuggestionsListView.setAdapter(adapter);
        startFilter(mSearchSrcTextView.getText());
    }
    
    public boolean isSearchOpen() {
        return mIsSearchOpen;
    }
    
    /**
     * Sets animation duration. ONLY FOR PRE-LOLLIPOP!!
     *
     * @param duration duration of the animation
     */
    public void setAnimationDuration(int duration) {
        mAnimationDuration = duration;
    }
    
    /**
     * Open Search View. This will animate the showing of the view.
     */
    public void showSearch() {
        applyTheme();
        showSearch(true);
    }
    
    /**
     * Open Search View. If animate is true, Animate the showing of the view.
     *
     * @param animate true for animate
     */
    public void showSearch(boolean animate) {
        if (isSearchOpen()) {
            return;
        }
        
        //Request Focus
        mSearchSrcTextView.setText(null);
        mSearchSrcTextView.requestFocus();
        
        if (animate) {
            setVisibleWithAnimation();
        } else {
            mSearchLayout.setVisibility(VISIBLE);
        }
        if (mSearchViewListener != null) {
            mSearchViewListener.onSearchViewShown();
        }
        mIsSearchOpen = true;
    }
    
    private void setVisibleWithAnimation() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mSearchLayout.setVisibility(View.VISIBLE);
            AnimationUtil.reveal(mSearchTopBar, null, true);
            
        } else {
            AnimationUtil.fadeInView(mSearchLayout, mAnimationDuration, null);
        }
    }
    
    /**
     * Close search view.
     */
    public void closeSearch() {
        if (!isSearchOpen()) {
            return;
        }
        
        mSearchSrcTextView.setText(null);
        dismissSuggestions();
        clearFocus();
    
        AnimationUtil.AnimationListener animationListener = new AnimationUtil.AnimationListener() {
            @Override
            public boolean onAnimationStart(View view) {
                return false;
            }
        
            @Override
            public boolean onAnimationEnd(View view) {
                mSearchLayout.setVisibility(GONE);
                if (mSearchViewListener != null) {
                    mSearchViewListener.onSearchViewClosed();
                }
                return false;
            }
        
            @Override
            public boolean onAnimationCancel(View view) {
                return false;
            }
        };
    
        AnimationUtil.reveal(mSearchTopBar, animationListener, false);

        mIsSearchOpen = false;
    }
    
    public void setOnQueryTextListener(OnQueryTextListener listener) {
        mOnQueryChangeListener = listener;
    }
    
    public void setOnSearchViewListener(SearchViewListener listener) {
        mSearchViewListener = listener;
    }
    
    public String getSearchEditText() {
        return String.valueOf(mSearchSrcTextView.getText());
    }
    
    @Override
    public void onFilterComplete(int count) {
        if (count > 0) {
            showSuggestions();
        } else {
            dismissSuggestions();
        }
    }
    
    @Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        // Don't accept focus if in the middle of clearing focus
        if (mClearingFocus) return false;
        // Check if SearchView is focusable.
        return isFocusable() && mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
    }
    
    @Override
    public void clearFocus() {
        mClearingFocus = true;
        hideKeyboard(this);
        super.clearFocus();
        mSearchSrcTextView.clearFocus();
        mClearingFocus = false;
    }
    
    private void applyTheme() {
        if (!Aequorea.getCurrentTheme().equals(mTheme)) {
            
            mTheme = Aequorea.getCurrentTheme();
    
            int lineColor = ThemeHelper.getResourceColor(mContext, R.attr.line_color);
            int rootColor = ThemeHelper.getResourceColor(mContext, R.attr.root_color);
            int textColor = ThemeHelper.getResourceColor(mContext, R.attr.title_color);
            int hintColor = ThemeHelper.getResourceColor(mContext, R.attr.subtitle_color);
    
            mSearchTopBar.setBackgroundColor(rootColor);
            mSuggestionsListView.setBackgroundColor(rootColor);
            mDividerView.setBackgroundColor(lineColor);
            mSearchSrcTextView.setTextColor(textColor);
            mSearchSrcTextView.setHintTextColor(hintColor);
    
            if (Aequorea.isLightTheme()) {
                mBackBtn.setImageResource(R.drawable.ic_action_navigation_arrow_back);
                mEmptyBtn.setImageResource(R.drawable.ic_action_navigation_close);
            } else {
                mBackBtn.setImageResource(R.drawable.ic_action_navigation_arrow_back_inverted);
                mEmptyBtn.setImageResource(R.drawable.ic_action_navigation_close_inverted);
            }
        }
    }
    
    public interface OnQueryTextListener {
        boolean onQueryTextSubmit(String query);

        boolean onQueryTextChange(String newText);
    }
    
    public interface SearchViewListener {
        void onSearchViewShown();
        
        void onSearchViewClosed();
    }
} @CoordinatorLayout.DefaultBehavior(NestedScrollAppBarLayout.Behaviour.class)
public class NestedScrollAppBarLayout extends AppBarLayout implements NestedScrollingChild {

    private int mAppLayoutHeight;

    private OnNestedScrollListener mListener;

    private ScrollAnimator mAnimator;

    public NestedScrollAppBarLayout(Context context) {
        super(context);
        initialize();
    }

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

    private void initialize() {
        setNestedScrollingEnabled(true);
    }

    public void setOnNestedListener(OnNestedScrollListener listener) {
        mListener = listener;
    }

    public interface OnNestedScrollListener {
        void onNestedScrolling();
        void onStopNestedScrolling();
    }

    public static class Behaviour extends AppBarLayout.Behavior {

        private NestedScrollAppBarLayout mAppBarLayout;
        private float mOriginTop;

        private void bindChild(AppBarLayout appBarLayout) {
            if (mAppBarLayout == null) {
                mAppBarLayout = (NestedScrollAppBarLayout) appBarLayout;
            }
        }
    
        @Override
        public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child, View directTargetChild, View target, int nestedScrollAxes, int type) {
            if (super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)) {
                bindChild(child);
                mOriginTop = child.getY();
                mAppBarLayout.stopScrollAnimation();
                return true;
            } else {
                return false;
            }
        }

        @Override
        public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dx, int dy, int[] consumed, int type) {
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        }

        @Override
        public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
            bindChild(child);
            if (mAppBarLayout.mListener != null) {
                mAppBarLayout.mListener.onNestedScrolling();
            }
        }

        @Override
        public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl, View target, int type) {
            bindChild(abl);

            float currentTop = abl.getTop();
            mAppBarLayout.mAppLayoutHeight = abl.getMeasuredHeight();

            if (currentTop > mOriginTop) {
                mAppBarLayout.scrollAndShowAppBar();
            } else if (currentTop < mOriginTop) {
                mAppBarLayout.scrollAndHideAppBar();
            }

            if (mAppBarLayout.mListener != null) {
                mAppBarLayout.mListener.onStopNestedScrolling();
            }
            super.onStopNestedScroll(coordinatorLayout, abl, target, type);
        }
    }

    private void scrollAndShowAppBar() {
        scrollToY(0);
    }

    private void scrollAndHideAppBar() {
        scrollToY(-mAppLayoutHeight);
    }

    private void scrollToY(int y) {
        if (getY() != y){
            mAnimator = new ScrollAnimator(y);
            mAnimator.start();
        }
    }

    private void stopScrollAnimation(){
        if (mAnimator != null)
            mAnimator.cancel();
    }

    @SuppressWarnings("unchecked")
    private class ScrollAnimator extends ValueAnimator {
        private CoordinatorLayout.Behavior behavior = null;
        private int lastY;

        ScrollAnimator(final int toY) {
            CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) getLayoutParams();
            behavior = lp.getBehavior();

            int fromY = (int) getY();
            lastY = fromY;

            setIntValues(fromY, toY);
            setDuration((long) (150.0 + 150.0 * Math.abs(toY - fromY) / getMeasuredHeight()));
            setInterpolator(new DecelerateInterpolator());
            addUpdateListener(new AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int currentY = (int) animation.getAnimatedValue();
                    int[] total = new int[]{0, lastY - currentY};
                    int[] consumed = new int[]{0, 0};
                    behavior.onNestedPreScroll((CoordinatorLayout) getParent(), NestedScrollAppBarLayout.this, NestedScrollAppBarLayout.this, total[0], total[1], consumed, ViewCompat.TYPE_NON_TOUCH);
                    behavior.onNestedScroll((CoordinatorLayout) getParent(), NestedScrollAppBarLayout.this, NestedScrollAppBarLayout.this, consumed[0], consumed[1],
                            total[0] - consumed[0], total[1] - consumed[1], ViewCompat.TYPE_NON_TOUCH);

                    lastY = currentY;
                }
            });
        }
    }
} public class StatusBarView extends View {
    
    public static final float LIGHT_INIT_MASK_ALPHA = 0.03f;
    public static final float LIGHT_MASK_ALPHA = 0.06f;
    public static final float DARK_INIT_MASK_ALPHA = 0.2f;
    public static final float DARKER_MASK_ALPHA = 0.2f;
    
    private boolean isInitState;
    private ObjectAnimator objectAnimator;
    
    public StatusBarView(Context context) {
        super(context);
        init(context, null, 0, 0);
    }
    
    public StatusBarView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0, 0);
    }
    
    public StatusBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr, 0);
    }
    
    public StatusBarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs, defStyleAttr, defStyleRes);
    }
    
    private void init(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.StatusBarView, defStyleAttr, defStyleRes);
        boolean isPlaceHolder = a.getBoolean(R.styleable.StatusBarView_placeholder, false);
        a.recycle();
        
        if (!isPlaceHolder) {
            setBackgroundResource(android.R.color.black);
            setAlpha(getAlphaFromDeviceApi());
        }
    }
    
    private float getAlphaFromDeviceApi() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            return LIGHT_INIT_MASK_ALPHA;
        } else {
            return DARKER_MASK_ALPHA;
        }
    }
    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), DisplayUtils.getStatusBarHeight(getResources()));
    }
    
    public void setDarkMask() {
        setInitState(false);
        changeAlpha(DARKER_MASK_ALPHA);
    }
    
    public void setLightMask() {
        setInitState(true);
        changeAlpha(LIGHT_INIT_MASK_ALPHA);
    }
    
    // only available in activity that does not need to change alpha
    public void setLight2ndMask() {
        setInitState(true);
        changeAlpha(LIGHT_MASK_ALPHA);
    }
    
    private void changeAlpha(float alphaTo) {
        cancelAnimator();
        if (getAlpha() != alphaTo) {
            objectAnimator = ObjectAnimator.ofFloat(this, "alpha", getAlpha(), alphaTo)
                .setDuration(150);
            objectAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
            objectAnimator.start();
        }
    }
    
    private void cancelAnimator() {
        if (objectAnimator != null) {
            objectAnimator.cancel();
        }
    }
    
    public void setInitState(boolean isInitState) {
        this.isInitState = isInitState;
    }
    
    public boolean isInitState() {
        return isInitState;
    }
} public class IntentUtils {
    public static void startSettingsActivity(Context context) {
        Intent i = new Intent(context, SettingsActivity.class);
        context.startActivity(i);
    }
    
    public static void startArticleActivity(Context context, long id) {
        Intent i = new Intent(context, ArticleActivity.class);
        i.putExtra(Constants.ARTICLE_ID, id);
        context.startActivity(i);
    }

    public static void startAuthorActivity(Context context, long id){
        Intent i = new Intent(context, AuthorActivity.class);
        i.putExtra(Constants.AUTHOR_ID, id);
        context.startActivity(i);
    }
    
    public static void startAuthorActivity(Context context, View avatar, Author author){
        Intent i = new Intent(context, AuthorActivity.class);
        i.putExtra(Constants.AUTHOR_ID, author.getId());
        i.putExtra(Constants.AUTHOR, author);
        ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) context, avatar, "author_avatar");
        context.startActivity(i, options.toBundle());
    }
    
    public static void startTagActivity(Context context, long id, String tag){
        Intent i = new Intent(context, TagActivity.class);
        i.putExtra(Constants.TAG, tag);
        i.putExtra(Constants.TAG_ID, id);
        context.startActivity(i);
    }
    
    public static void startSearchActivity(Context context, String key){
        Intent i = new Intent(context, SearchActivity.class);
        i.putExtra(Constants.SEARCH_KEY, key);
        context.startActivity(i);
    }
    
    public static void shareText(Context context, String title, String content, String dialogTitle){
        Intent share_intent = new Intent();
        share_intent.setAction(Intent.ACTION_SEND);
        share_intent.setType("text/plain");
        share_intent.putExtra(Intent.EXTRA_SUBJECT, title);
        share_intent.putExtra(Intent.EXTRA_TEXT, content);
        share_intent = Intent.createChooser(share_intent, dialogTitle);
        context.startActivity(share_intent);
    }
    
    public static void openInBrowser(Context context, String url) {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_VIEW);
        Uri content_url = Uri.parse(url);
        intent.setData(content_url);
        context.startActivity(intent);
    }
    
    public static void openImageFromStorage(Context context, String path) {
        Intent intent = new Intent();
        File file = new File(path);
        Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, file);
    
        intent.setAction(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, "image/jpg");
        intent.addCategory(Intent.CATEGORY_DEFAULT);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        context.startActivity(intent);
    }
    
    public static void openInNewPhotoActivity(Context context, String url) {
        Intent intent = new Intent(context, PhotoActivity.class);
        intent.putExtra(Constants.PHOTO, url);
        context.startActivity(intent);
    }
} public class NetworkUtils {

    public static boolean isNetworkAvailable() {
        Context context = Aequorea.getApp();
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    
        NetworkInfo info = null;
        if (connectivityManager != null) {
            info = connectivityManager.getActiveNetworkInfo();
        }
        return info != null && info.isAvailable();
    }
    
    public static boolean isWiFiNetwork() {
        Context context = Aequorea.getApp();
        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    
        NetworkInfo info = null;
        if (connectivityManager != null) {
            info = connectivityManager.getActiveNetworkInfo();
        }
    
        return info != null && info.getType() == ConnectivityManager.TYPE_WIFI && info.isAvailable();
    }
} public class SPUtils {
    private static SharedPreferences mSp;
    
    private static SharedPreferences getSp() {
        if (mSp == null) {
            mSp = Aequorea.getApp().getSharedPreferences("Aequorea", Context.MODE_PRIVATE);
        }
        return mSp;
    }
    
    public static void setString(String key, String value) {
        getSp().edit().putString(key, value).apply();
    }
    
    public static String getString(String key) {
        return getSp().getString(key, "");
    }
    
    public static String getString(String key, String defaultValue) {
        return getSp().getString(key, defaultValue);
    }
    
    public static void setInt(String key, int value) {
        getSp().edit().putInt(key, value).apply();
    }
    
    public static int getInt(String key) {
        return getSp().getInt(key, 0);
    }
    
    public static int getInt(String key, int defaultValue) {
        return getSp().getInt(key, defaultValue);
    }
    
    public static void setBoolean(String key, boolean value) {
        getSp().edit().putBoolean(key, value).apply();
    }
    
    public static boolean getBoolean(String key) {
        return getSp().getBoolean(key, false);
    }
} public class SnackbarUtils {
    
    public static void show(View view, String content) {
        Snackbar.make(view, content, Snackbar.LENGTH_SHORT).show();
    }
    
    public static void show(View view, String content, int duration) {
        Snackbar.make(view, content, duration).show();
    }
    
    public static Snackbar getSnackbar(View view, String content, int duration) {
        return Snackbar.make(view, content, duration);
    }
} public class ThemeHelper {
    public static String getTheme() {
        String theme = SPUtils.getString(Constants.THEME);
        return "".equals(theme) ? Constants.THEME_LIGHT : theme;
    }
    
    public static void setTheme(String theme) {
        SPUtils.setString(Constants.THEME, theme);
        Aequorea.setCurrentTheme(theme);
    }
    
    public static int getThemeStyle(String theme) {
        switch (theme) {
            default:
            case Constants.THEME_LIGHT:
                return R.style.AppTheme_Light;
            case Constants.THEME_DARK:
                return R.style.AppTheme_Dark;
        }
    }
    
    public static int getArticleThemeStyle(String theme) {
        switch (theme) {
            default:
            case Constants.THEME_LIGHT:
                return R.style.AppTheme_Article_Light;
            case Constants.THEME_DARK:
                return R.style.AppTheme_Article_Dark;
        }
    }
    
    public static int getResourceId(Context context, int attr) {
        TypedValue typedValue = new TypedValue();
        context.getTheme().resolveAttribute(attr, typedValue, true);
        return typedValue.resourceId;
    }
    
    public static int getResourceColor(Context context, int attr){
        TypedValue typedValue = new TypedValue();
        context.getTheme().resolveAttribute(attr, typedValue, true);
        return ContextCompat.getColor(context, typedValue.resourceId);
    }
} public class MainPageModel extends BaseModel{

} public class Datum {

    @SerializedName("anonymous_purchase_total_count")
    private Long mAnonymousPurchaseTotalCount;
    @SerializedName("anonymous_purchases")
    private List<AnonymousPurchase> mAnonymousPurchases;
    @SerializedName("anonymous_share_price")
    private String mAnonymousSharePrice;
    @SerializedName("article_type")
    private String mArticleType;
    @SerializedName("authors")
    private List<Author> mAuthors;
    @SerializedName("column")
    private Column mColumn;
    @SerializedName("comment_times")
    private Long mCommentTimes;
    @SerializedName("data")
    private List<Datum> mData;
    @SerializedName("display_time")
    private String mDisplayTime;
    @SerializedName("editor_choice_comments")
    private List<Object> mEditorChoiceComments;
    @SerializedName("is_favorited")
    private Boolean mIsFavorited;
    @SerializedName("is_like")
    private Boolean mIsLike;
    @SerializedName("like_times")
    private Long mLikeTimes;
    @SerializedName("magazine")
    private Magazine mMagazine;
    @SerializedName("read_time")
    private Long mReadTime;
    @SerializedName("share_url")
    private String mShareUrl;
    @SerializedName("share_visit_limit")
    private Long mShareVisitLimit;
    @SerializedName("share_visit_times")
    private Long mShareVisitTimes;
    @SerializedName("summary")
    private String mSummary;
    @SerializedName("title")
    private String mTitle;
    @SerializedName("topics")
    private List<Topic> mTopics;
    @SerializedName("type")
    private String mType;
    @SerializedName("visit_times")
    private Long mVisitTimes;
    @SerializedName("articles_count")
    private Long mArticlesCount;
    private String mCoverUrl;
    @SerializedName("description")
    private Object mDescription;
    @SerializedName("id")
    private Long mId;
    @SerializedName("illustration")
    private String mIllustration;
    @SerializedName("name")
    private String mName;
    @SerializedName("content")
    private String mContent;
    @SerializedName("total_read_times")
    private Long mTotalReadTimes;
    @SerializedName("total_visit_times")
    private Long mTotalVisitTimes;

    public Long getArticlesCount() {
        return mArticlesCount;
    }

    public void setArticlesCount(Long articlesCount) {
        mArticlesCount = articlesCount;
    }


    public Object getDescription() {
        return mDescription;
    }

    public void setDescription(Object description) {
        mDescription = description;
    }

    public String getIllustration() {
        return mIllustration;
    }

    public void setIllustration(String illustration) {
        mIllustration = illustration;
    }

    public String getName() {
        return mName;
    }

    public void setName(String name) {
        mName = name;
    }

    public Long getTotalReadTimes() {
        return mTotalReadTimes;
    }

    public void setTotalReadTimes(Long totalReadTimes) {
        mTotalReadTimes = totalReadTimes;
    }

    public Long getTotalVisitTimes() {
        return mTotalVisitTimes;
    }

    public void setTotalVisitTimes(Long totalVisitTimes) {
        mTotalVisitTimes = totalVisitTimes;
    }


    public Long getAnonymousPurchaseTotalCount() {
        return mAnonymousPurchaseTotalCount;
    }

    public void setAnonymousPurchaseTotalCount(Long anonymousPurchaseTotalCount) {
        mAnonymousPurchaseTotalCount = anonymousPurchaseTotalCount;
    }

    public List<AnonymousPurchase> getAnonymousPurchases() {
        return mAnonymousPurchases;
    }

    public void setAnonymousPurchases(List<AnonymousPurchase> anonymousPurchases) {
        mAnonymousPurchases = anonymousPurchases;
    }

    public String getAnonymousSharePrice() {
        return mAnonymousSharePrice;
    }

    public void setAnonymousSharePrice(String anonymousSharePrice) {
        mAnonymousSharePrice = anonymousSharePrice;
    }

    public String getArticleType() {
        return mArticleType;
    }

    public void setArticleType(String articleType) {
        mArticleType = articleType;
    }

    public List<Author> getAuthors() {
        return mAuthors;
    }

    public void setAuthors(List<Author> authors) {
        mAuthors = authors;
    }

    public Column getColumn() {
        return mColumn;
    }

    public void setColumn(Column column) {
        mColumn = column;
    }

    public Long getCommentTimes() {
        return mCommentTimes;
    }

    public void setCommentTimes(Long commentTimes) {
        mCommentTimes = commentTimes;
    }

    public String getCoverUrl() {
        return mCoverUrl;
    }

    public void setCoverUrl(String coverUrl) {
        mCoverUrl = coverUrl;
    }

    public List<Datum> getData() {
        return mData;
    }

    public void setData(List<Datum> data) {
        mData = data;
    }

    public String getDisplayTime() {
        return mDisplayTime;
    }

    public void setDisplayTime(String displayTime) {
        mDisplayTime = displayTime;
    }

    public List<Object> getEditorChoiceComments() {
        return mEditorChoiceComments;
    }

    public void setEditorChoiceComments(List<Object> editorChoiceComments) {
        mEditorChoiceComments = editorChoiceComments;
    }

    public Long getId() {
        return mId;
    }

    public void setId(Long id) {
        mId = id;
    }

    public Boolean getIsFavorited() {
        return mIsFavorited;
    }

    public void setIsFavorited(Boolean isFavorited) {
        mIsFavorited = isFavorited;
    }

    public Boolean getIsLike() {
        return mIsLike;
    }

    public void setIsLike(Boolean isLike) {
        mIsLike = isLike;
    }

    public Long getLikeTimes() {
        return mLikeTimes;
    }

    public void setLikeTimes(Long likeTimes) {
        mLikeTimes = likeTimes;
    }

    public Magazine getMagazine() {
        return mMagazine;
    }

    public void setMagazine(Magazine magazine) {
        mMagazine = magazine;
    }

    public Long getReadTime() {
        return mReadTime;
    }

    public void setReadTime(Long readTime) {
        mReadTime = readTime;
    }

    public String getShareUrl() {
        return mShareUrl;
    }

    public void setShareUrl(String shareUrl) {
        mShareUrl = shareUrl;
    }

    public Long getShareVisitLimit() {
        return mShareVisitLimit;
    }

    public void setShareVisitLimit(Long shareVisitLimit) {
        mShareVisitLimit = shareVisitLimit;
    }

    public Long getShareVisitTimes() {
        return mShareVisitTimes;
    }

    public void setShareVisitTimes(Long shareVisitTimes) {
        mShareVisitTimes = shareVisitTimes;
    }

    public String getSummary() {
        return mSummary;
    }

    public void setSummary(String summary) {
        mSummary = summary;
    }

    public String getTitle() {
        return mTitle;
    }

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

    public List<Topic> getTopics() {
        return mTopics;
    }

    public void setTopics(List<Topic> topics) {
        mTopics = topics;
    }

    public String getType() {
        return mType;
    }

    public void setType(String type) {
        mType = type;
    }

    public Long getVisitTimes() {
        return mVisitTimes;
    }

    public void setVisitTimes(Long visitTimes) {
        mVisitTimes = visitTimes;
    }
    
    public String getContent() {
        return mContent;
    }
    
    public void setContent(String content) {
        mContent = content;
    }
} public class Content {

    @SerializedName("article_type")
    private String mArticleType;
    @SerializedName("authors")
    private List<Author> mAuthors;
    @SerializedName("cover_url")
    private String mCoverUrl;
    @SerializedName("id")
    private Long mId;
    @SerializedName("publish_at")
    private String mPublishAt;
    @SerializedName("summary")
    private String mSummary;
    @SerializedName("title")
    private String mTitle;

    public String getArticleType() {
        return mArticleType;
    }

    public void setArticleType(String articleType) {
        mArticleType = articleType;
    }

    public List<Author> getAuthors() {
        return mAuthors;
    }

    public void setAuthors(List<Author> authors) {
        mAuthors = authors;
    }

    public String getCoverUrl() {
        return mCoverUrl;
    }

    public void setCoverUrl(String coverUrl) {
        mCoverUrl = coverUrl;
    }

    public Long getId() {
        return mId;
    }

    public void setId(Long id) {
        mId = id;
    }

    public String getPublishAt() {
        return mPublishAt;
    }

    public void setPublishAt(String publishAt) {
        mPublishAt = publishAt;
    }

    public String getSummary() {
        return mSummary;
    }

    public void setSummary(String summary) {
        mSummary = summary;
    }

    public String getTitle() {
        return mTitle;
    }

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

} public class SearchDatum {

    @SerializedName("content")
    private Content mContent;
    @SerializedName("searchable_id")
    private Long mSearchableId;
    @SerializedName("searchable_type")
    private String mSearchableType;

    public Content getContent() {
        return mContent;
    }

    public void setContent(Content content) {
        mContent = content;
    }

    public Long getSearchableId() {
        return mSearchableId;
    }

    public void setSearchableId(Long searchableId) {
        mSearchableId = searchableId;
    }

    public String getSearchableType() {
        return mSearchableType;
    }

    public void setSearchableType(String searchableType) {
        mSearchableType = searchableType;
    }

} public class MainPresenter extends BasePresenter<HomeView> {
    private NetworkService mNetworkService;
    private Timer mTimer;
    
    private static final int ITEM_PER_PAGE = 15;
    
    private static final int INSTANT_SEARCH_DELAY = 300;
    
    private int mPage = 1;
    
    @Override
    protected void onAttach() {
        mNetworkService = RequestManager.getInstance().getRetrofit().create(NetworkService.class);
        mTimer = new Timer();
        loadCacheData();
    }
    
    private void loadCacheData() {
        if (mPage == 1 && !mBaseView.getModel().isRefreshing()) {
            Data cacheData = useCache();
            if (cacheData != null) {
                onDataLoaded(cacheData);
            }
        }
    }
    
    public void loadData() {
        loadData(-1);
    }
    
    public void loadData(int page) {
        if (!NetworkUtils.isNetworkAvailable()) {
            onError(new Throwable(getString(R.string.please_connect_to_the_internet)));
            return;
        }
        
        if (mBaseView.getModel().isLoading()){
            return;
        }
        
        if (page != -1) {
            mPage = page;
        }
    
        mBaseView.getModel().setLoading(true);
    
        mComposite.add(mNetworkService
                .getMainPageInfo(mPage, ITEM_PER_PAGE)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Data>() {
                    @Override
                    public void accept(Data data) throws Exception {
                        if (mPage == 1) {
                            cacheData(data);
                        }
                        mPage++;
                        onDataLoaded(data);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        onError(throwable);
                    }
                })
        );
    }
    
    private void onDataLoaded(Data data) {
        mBaseView.onDataLoaded(FilterUtils.filterData(data.getData()), mBaseView.getModel().isRefreshing());
        setLoadingFinish();
    }
    
    private void onError(Throwable t) {
        setLoadingFinish();
        mBaseView.onError(t);
    }
    
    private void setLoadingFinish() {
        mBaseView.setRefreshing(false);
        mBaseView.getModel().setLoading(false);
        mBaseView.getModel().setRefreshing(false);
    }

    public void refresh() {
        loadData(1);
    }
    
    private void cacheData(Data data) {
        Gson gson = new Gson();
        String cacheString = gson.toJson(data);
        SPUtils.setString(Constants.SP_LATEST_MAIN_PAGE, cacheString);
    }
    
    private Data useCache() {
        String cacheString = SPUtils.getString(Constants.SP_LATEST_MAIN_PAGE);
        if (!TextUtils.isEmpty(cacheString)) {
            Gson gson = new Gson();
            return gson.fromJson(cacheString, Data.class);
        }
        return null;
    }
    
    public void getSearchList(final String key) {
        if (TextUtils.isEmpty(key)) return;
        
        mTimer.cancel();
        mTimer = new Timer();
        mTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                getSearchListAfterDelay(key);
            }
        }, INSTANT_SEARCH_DELAY);
    }
    
    private void getSearchListAfterDelay(String key) {
    
        mComposite.add(mNetworkService
            .getArticleListWithKeyword(1, 10, key, false, "article")
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Consumer<SearchData>() {
                @Override
                public void accept(SearchData data) throws Exception {
                    onSearchResultLoaded(data);
                }
            }, new Consumer<Throwable>() {
                @Override
                public void accept(Throwable throwable) throws Exception {
                    onError(throwable);
                }
            })
        );
    }
    
    private void onSearchResultLoaded(SearchData data) {
        mBaseView.onSearchResultLoaded(FilterUtils.filterSearchData(data.getData()));
    }
} public class InstantSearchAdapter extends BaseAdapter {
    
    private ArrayList<InstantSearchBean> mData;
    private LayoutInflater mInflater;
    private int mActionBarSize;
    
    public InstantSearchAdapter(Context context, ArrayList<InstantSearchBean> instantSearchBeen) {
        mInflater = LayoutInflater.from(context);
        mData = instantSearchBeen;
        mActionBarSize = (int) context.getResources().getDimension(R.dimen.search_item_height);
    }
    
    @Override
    public int getCount() {
        return mData == null ? 0 : mData.size();
    }
    
    @Override
    public Object getItem(int position) {
        return mData.get(position);
    }
    
    @Override
    public long getItemId(int position) {
        return position;
    }
    
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
    
        InstantSearchAdapter.SuggestionsViewHolder viewHolder;
        
        if (convertView == null) {
            convertView = mInflater.inflate(R.layout.item_search, parent, false);
            viewHolder = new InstantSearchAdapter.SuggestionsViewHolder(convertView);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (InstantSearchAdapter.SuggestionsViewHolder) convertView.getTag();
        }
        
        String currentListData = ((InstantSearchBean) getItem(position)).title;
        
        viewHolder.textView.setText(currentListData);
        convertView.setTag(R.string.app_name, mData.get(position));
        
        return convertView;
    }
    
    public int getListHeight() {
        return mActionBarSize * getCount();
    }
    
    private class SuggestionsViewHolder {
        
        private TextView textView;
        
        private SuggestionsViewHolder(View convertView) {
            textView = convertView.findViewById(R.id.search_text);
        }
    }
    
    public static class InstantSearchBean {
        public long id;
        public String title;
    
        public InstantSearchBean(long id, String title) {
            this.id = id;
            this.title = title;
        }
    }
} public class MainArticleAdapter extends RecyclerView.Adapter<MainArticleAdapter.ViewHolder> {
    
    private LayoutInflater mInflater;
    private Context mContext;
    
    private List<Datum> mArticleList;
    private Set<Long> mIdSet;
    
    public MainArticleAdapter(Context context, List<Datum> articleList) {
        this.mContext = context;
        this.mInflater = LayoutInflater.from(context);
        this.mArticleList = articleList;
        this.mIdSet = new HashSet<>();
    }
    
    public void setArticleList(List<Datum> articleList, boolean isRefresh) {
        if (this.mArticleList == null) {
            this.mArticleList = articleList;
        } else if (isRefresh) {
            int i = 0;
            for (Datum d : articleList) {
                if (!listContains(d)) {
                    this.mArticleList.add(i++, d);
                }
            }
        } else {
            for (Datum d : articleList) {
                if (!listContains(d)) {
                    this.mArticleList.add(d);
                }
            }
        }
        notifyDataSetChanged();
        updateIdSet();
    }
    
    private void updateIdSet() {
        int size = this.mArticleList.size();
        for (int i = 0; i < size; i++) {
            mIdSet.add(this.mArticleList.get(i).getData().get(0).getId());
        }
    }
    
    private boolean listContains(Datum d) {
        Long id = d.getData().get(0).getId();
        return id != null && mIdSet.contains(id);
    }
    
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new NormalArticleHolder(mInflater.inflate(R.layout.item_article, parent, false));
    }
    
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        holder.onBindView(mContext, mArticleList.get(position));
    }
    
    @Override
    public int getItemCount() {
        return mArticleList == null ? 0 : mArticleList.size();
    }
    
    public static abstract class ViewHolder extends RecyclerView.ViewHolder {
        public ViewHolder(View itemView) {
            super(itemView);
        }
        
        public abstract void onBindView(Context context, Datum data);
    }
} public interface HomeView extends NetworkView {
    void onDataLoaded(List<Datum> datumList, boolean isRefresh);
    
    void setRefreshing(boolean b);
    
    void onSearchResultLoaded(List<SearchDatum> data);
    
    MainPageModel getModel();
}
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.appcompat.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ProgressBar;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import nich.work.aequorea.Aequorea;
import nich.work.aequorea.R;
import nich.work.aequorea.common.Constants;
import nich.work.aequorea.common.service.CacheService;
import nich.work.aequorea.common.ui.activity.BaseActivity;
import nich.work.aequorea.common.ui.widget.MaterialSearchView;
import nich.work.aequorea.common.ui.widget.NestedScrollAppBarLayout;
import nich.work.aequorea.common.ui.widget.StatusBarView;
import nich.work.aequorea.common.utils.IntentUtils;
import nich.work.aequorea.common.utils.NetworkUtils;
import nich.work.aequorea.common.utils.SPUtils;
import nich.work.aequorea.common.utils.SnackbarUtils;
import nich.work.aequorea.common.utils.ThemeHelper;
import nich.work.aequorea.model.MainPageModel;
import nich.work.aequorea.model.entity.Datum;
import nich.work.aequorea.model.entity.search.Content;
import nich.work.aequorea.model.entity.search.SearchDatum;
import nich.work.aequorea.presenter.MainPresenter;
import nich.work.aequorea.ui.adapter.InstantSearchAdapter;
import nich.work.aequorea.ui.adapter.MainArticleAdapter;
import nich.work.aequorea.ui.view.HomeView;
package nich.work.aequorea.ui.activitiy;





public class MainActivity extends BaseActivity implements HomeView, NestedScrollAppBarLayout.OnNestedScrollListener, View.OnClickListener {
    
    private static final String TAG = MainActivity.class.getSimpleName();
    
    private MainPresenter mPresenter;
    private MainArticleAdapter mAdapter;
    private MainPageModel mModel;
    private LinearLayoutManager mLinearLayoutManager;
    private MenuItem mSearchMenuItem;
    
    private long mClickTime;
    
    @BindView(R.id.rec)
    protected RecyclerView mRecyclerView;
    @BindView(R.id.toolbar)
    protected Toolbar mToolbar;
    @BindView(R.id.main_content)
    protected CoordinatorLayout mCoordinatorLayout;
    @BindView(R.id.appbar)
    protected NestedScrollAppBarLayout mAppBarLayout;
    @BindView(R.id.status_bar)
    protected StatusBarView mStatusBar;
    @BindView(R.id.layout_swipe_refresh)
    protected SwipeRefreshLayout mSwipeRefreshLayout;
    @BindView(R.id.loading_progressbar)
    protected ProgressBar mProgressBar;
    @BindView(R.id.container_refresh)
    protected View mRefreshView;
    @BindView(R.id.search_view)
    protected MaterialSearchView mSearchView;
    @BindView(R.id.search_mask)
    protected View mSearchMask;
    
    @OnClick(R.id.container_refresh)
    protected void refresh() {
        hideRefreshLayout();
        mProgressBar.setVisibility(View.VISIBLE);
        mPresenter.refresh();
    }
    
    @OnClick (R.id.search_mask)
    protected void hideMask() {
        if (mSearchMask.getVisibility() == View.VISIBLE) {
            mSearchView.closeSearch();
            mSearchMask.setVisibility(View.GONE);
        }
    }
    
    private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(RecyclerView mRecyclerView, int dx, int dy) {
            super.onScrolled(mRecyclerView, dx, dy);
            if (dy > Constants.AUTO_LOAD_TRIGGER) {