com.mishiranu.dashchan.ui.navigator.DrawerForm.java Source code

Java tutorial

Introduction

Here is the source code for com.mishiranu.dashchan.ui.navigator.DrawerForm.java

Source

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

package com.mishiranu.dashchan.ui.navigator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.support.v4.widget.DrawerLayout;
import android.text.InputFilter;
import android.text.InputType;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import chan.content.ChanConfiguration;
import chan.content.ChanLocator;
import chan.content.ChanManager;
import chan.util.StringUtils;

import com.mishiranu.dashchan.C;
import com.mishiranu.dashchan.R;
import com.mishiranu.dashchan.content.service.WatcherService;
import com.mishiranu.dashchan.content.storage.FavoritesStorage;
import com.mishiranu.dashchan.preference.Preferences;
import com.mishiranu.dashchan.ui.navigator.page.PageHolder;
import com.mishiranu.dashchan.util.DialogMenu;
import com.mishiranu.dashchan.util.FlagUtils;
import com.mishiranu.dashchan.util.GraphicsUtils;
import com.mishiranu.dashchan.util.ListViewUtils;
import com.mishiranu.dashchan.util.NavigationUtils;
import com.mishiranu.dashchan.util.ResourceUtils;
import com.mishiranu.dashchan.util.ToastUtils;
import com.mishiranu.dashchan.widget.EdgeEffectHandler;
import com.mishiranu.dashchan.widget.SafePasteEditText;
import com.mishiranu.dashchan.widget.SortableListView;

public class DrawerForm extends BaseAdapter
        implements EdgeEffectHandler.Shift, SortableListView.OnFinishedListener, AdapterView.OnItemClickListener,
        AdapterView.OnItemLongClickListener, DrawerLayout.DrawerListener, EditText.OnEditorActionListener,
        View.OnClickListener, View.OnTouchListener, WatcherService.Client.Callback {
    private final Context context;
    private final Context unstyledContext;
    private final Callback callback;

    private final InputMethodManager inputMethodManager;

    private final EditText searchEdit;
    private final View headerView;
    private final TextView chanNameView;
    private final ImageView chanSelectorIcon;

    private final HashMap<String, Drawable> chanIcons = new HashMap<>();
    private final HashSet<String> watcherSupportSet = new HashSet<>();

    private final ArrayList<ListItem> chans = new ArrayList<>();
    private final ArrayList<ListItem> pages = new ArrayList<>();
    private final ArrayList<ListItem> favorites = new ArrayList<>();
    private final ArrayList<ArrayList<ListItem>> categories = new ArrayList<>();
    private final ArrayList<ListItem> menu = new ArrayList<>();

    private final WatcherService.Client watcherServiceClient;
    private SortableListView listView;

    private boolean mergeChans = false;
    private boolean chanSelectMode = false;
    private String chanName;

    public static final int RESULT_REMOVE_ERROR_MESSAGE = 0x00000001;
    public static final int RESULT_SUCCESS = 0x00000002;

    public static final int MENU_ITEM_ALL_BOARDS = 1;
    public static final int MENU_ITEM_USER_BOARDS = 2;
    public static final int MENU_ITEM_HISTORY = 3;
    public static final int MENU_ITEM_PREFERENCES = 4;

    public interface Callback {
        public void onSelectChan(String chanName);

        public void onSelectBoard(String chanName, String boardName, boolean fromCache);

        public boolean onSelectThread(String chanName, String boardName, String threadNumber, String postNumber,
                String threadTitle, boolean fromCache);

        public boolean onClosePage(String chanName, String boardName, String threadNumber);

        public void onCloseAllPages();

        public int onEnterNumber(int number);

        public void onSelectDrawerMenuItem(int item);

        public ArrayList<PageHolder> getDrawerPageHolders();
    }

    public DrawerForm(Context context, Context unstyledContext, Callback callback,
            WatcherService.Client watcherServiceClient) {
        this.context = context;
        this.unstyledContext = unstyledContext;
        this.callback = callback;
        this.watcherServiceClient = watcherServiceClient;
        float density = ResourceUtils.obtainDensity(context);
        LinearLayout linearLayout = new LinearLayout(context);
        linearLayout.setOrientation(LinearLayout.VERTICAL);
        linearLayout.setLayoutParams(new SortableListView.LayoutParams(SortableListView.LayoutParams.MATCH_PARENT,
                SortableListView.LayoutParams.WRAP_CONTENT));
        LinearLayout editTextContainer = new LinearLayout(context);
        editTextContainer.setGravity(Gravity.CENTER_VERTICAL);
        linearLayout.addView(editTextContainer);
        searchEdit = new SafePasteEditText(context);
        searchEdit.setOnKeyListener((v, keyCode, event) -> {
            if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
                v.clearFocus();
            }
            return false;
        });
        searchEdit.setHint(context.getString(R.string.text_code_number_address));
        searchEdit.setOnEditorActionListener(this);
        searchEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
        searchEdit.setImeOptions(EditorInfo.IME_ACTION_GO | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
        ImageView searchIcon = new ImageView(context, null, android.R.attr.buttonBarButtonStyle);
        searchIcon.setImageResource(ResourceUtils.getResourceId(context, R.attr.buttonForward, 0));
        searchIcon.setScaleType(ImageView.ScaleType.CENTER);
        searchIcon.setOnClickListener(this);
        editTextContainer.addView(searchEdit,
                new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1));
        editTextContainer.addView(searchIcon, (int) (40f * density), (int) (40f * density));
        if (C.API_LOLLIPOP) {
            editTextContainer.setPadding((int) (12f * density), (int) (8f * density), (int) (8f * density), 0);
        } else {
            editTextContainer.setPadding(0, (int) (2f * density), (int) (4f * density), (int) (2f * density));
        }
        LinearLayout selectorContainer = new LinearLayout(context);
        selectorContainer.setBackgroundResource(
                ResourceUtils.getResourceId(context, android.R.attr.selectableItemBackground, 0));
        selectorContainer.setOrientation(LinearLayout.HORIZONTAL);
        selectorContainer.setGravity(Gravity.CENTER_VERTICAL);
        selectorContainer.setOnClickListener(v -> {
            hideKeyboard();
            setChanSelectMode(!chanSelectMode);
        });
        linearLayout.addView(selectorContainer);
        selectorContainer.setMinimumHeight((int) (40f * density));
        if (C.API_LOLLIPOP) {
            selectorContainer.setPadding((int) (16f * density), 0, (int) (16f * density), 0);
            ((LinearLayout.LayoutParams) selectorContainer.getLayoutParams()).topMargin = (int) (4f * density);
        } else {
            selectorContainer.setPadding((int) (8f * density), 0, (int) (12f * density), 0);
        }
        chanNameView = new TextView(context, null, android.R.attr.textAppearanceListItem);
        chanNameView.setTextSize(TypedValue.COMPLEX_UNIT_SP, C.API_LOLLIPOP ? 14f : 16f);
        if (C.API_LOLLIPOP) {
            chanNameView.setTypeface(GraphicsUtils.TYPEFACE_MEDIUM);
        } else {
            chanNameView.setFilters(new InputFilter[] { new InputFilter.AllCaps() });
        }
        selectorContainer.addView(chanNameView,
                new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1));
        chanSelectorIcon = new ImageView(context);
        chanSelectorIcon.setImageResource(ResourceUtils.getResourceId(context, R.attr.buttonDropDownDrawer, 0));
        selectorContainer.addView(chanSelectorIcon, (int) (24f * density), (int) (24f * density));
        ((LinearLayout.LayoutParams) chanSelectorIcon.getLayoutParams()).gravity = Gravity.CENTER_VERTICAL
                | Gravity.END;
        headerView = linearLayout;
        inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
        chans.add(new ListItem(ListItem.ITEM_DIVIDER, 0, 0, null));
        int color = ResourceUtils.getColor(context, R.attr.drawerIconColor);
        ChanManager manager = ChanManager.getInstance();
        Collection<String> availableChans = manager.getAvailableChanNames();
        for (String chanName : availableChans) {
            ChanConfiguration configuration = ChanConfiguration.get(chanName);
            if (configuration.getOption(ChanConfiguration.OPTION_READ_POSTS_COUNT)) {
                watcherSupportSet.add(chanName);
            }
            Drawable drawable = manager.getIcon(chanName, color);
            chanIcons.put(chanName, drawable);
            chans.add(
                    new ListItem(ListItem.ITEM_CHAN, chanName, null, null, configuration.getTitle(), 0, drawable));
        }
        if (availableChans.size() == 1) {
            selectorContainer.setVisibility(View.GONE);
        }
    }

    public void bind(SortableListView listView) {
        this.listView = listView;
        listView.setClipToPadding(false);
        listView.setEdgeEffectShift(this);
        listView.setScrollBarStyle(SortableListView.SCROLLBARS_OUTSIDE_OVERLAY);
        listView.setAdapter(this);
        listView.setOnItemClickListener(this);
        listView.setOnItemLongClickListener(this);
        listView.setOnScrollListener(new ListViewScrollFixListener());
        listView.setOnSortingFinishedListener(this);
        listView.setOnTouchListener(this);
        float density = ResourceUtils.obtainDensity(context);
        if (C.API_LOLLIPOP) {
            listView.setDivider(null);
        } else {
            listView.setPadding((int) (12f * density), 0, (int) (12f * density), 0);
        }
    }

    public void updateConfiguration(String chanName) {
        if (!StringUtils.equals(chanName, this.chanName)) {
            this.chanName = chanName;
            ChanConfiguration configuration = ChanConfiguration.get(chanName);
            chanNameView.setText(configuration.getTitle());
            menu.clear();
            Context context = this.context;
            menu.add(new ListItem(ListItem.ITEM_DIVIDER, 0, 0, null));
            TypedArray typedArray = context.obtainStyledAttributes(new int[] { R.attr.drawerMenuBoards,
                    R.attr.drawerMenuUserBoards, R.attr.drawerMenuHistory, R.attr.drawerMenuPreferences });
            boolean addDivider = false;
            boolean hasUserBoards = configuration.getOption(ChanConfiguration.OPTION_READ_USER_BOARDS);
            if (!configuration.getOption(ChanConfiguration.OPTION_SINGLE_BOARD_MODE)) {
                menu.add(new ListItem(ListItem.ITEM_MENU, MENU_ITEM_ALL_BOARDS, typedArray.getResourceId(0, 0),
                        context.getString(
                                hasUserBoards ? R.string.action_general_boards : R.string.action_boards)));
                addDivider = true;
            }
            if (hasUserBoards) {
                menu.add(new ListItem(ListItem.ITEM_MENU, MENU_ITEM_USER_BOARDS, typedArray.getResourceId(1, 0),
                        context.getString(R.string.action_user_boards)));
                addDivider = true;
            }
            if (addDivider) {
                menu.add(new ListItem(ListItem.ITEM_DIVIDER, 0, 0, null));
            }
            menu.add(new ListItem(ListItem.ITEM_MENU, MENU_ITEM_HISTORY, typedArray.getResourceId(2, 0),
                    context.getString(R.string.action_history)));
            menu.add(new ListItem(ListItem.ITEM_MENU, MENU_ITEM_PREFERENCES, typedArray.getResourceId(3, 0),
                    context.getString(R.string.action_preferences)));
            typedArray.recycle();
            invalidateItems(false, true);
        }
    }

    public View getHeaderView() {
        return headerView;
    }

    public void setChanSelectMode(boolean enabled) {
        if (chans.size() > 2 && chanSelectMode != enabled) {
            chanSelectMode = enabled;
            chanSelectorIcon.setRotation(enabled ? 180f : 0f);
            notifyDataSetChanged();
            listView.setSelection(0);
        }
    }

    @Override
    public void onSortingFinished(SortableListView listView, int oldPosition, int newPosition) {
        if (chanSelectMode) {
            chans.add(newPosition - 1, chans.remove(oldPosition - 1));
            ArrayList<String> chanNames = new ArrayList<>();
            for (ListItem listItem : chans) {
                if (listItem.type == ListItem.ITEM_CHAN) {
                    chanNames.add(listItem.chanName);
                }
            }
            ChanManager.getInstance().setChansOrder(chanNames);
            // Regroup favorite threads
            if (mergeChans) {
                invalidateItems(false, true);
            }
        } else {
            ListItem listItem = getItem(oldPosition);
            ListItem afterListItem = newPosition > oldPosition ? getItem(newPosition) : getItem(newPosition - 1);
            FavoritesStorage favoritesStorage = FavoritesStorage.getInstance();
            FavoritesStorage.FavoriteItem favoriteItem = favoritesStorage.getFavorite(listItem.chanName,
                    listItem.boardName, listItem.threadNumber);
            FavoritesStorage.FavoriteItem afterFavoriteItem = afterListItem.type == ListItem.ITEM_FAVORITE
                    && afterListItem.chanName.equals(favoriteItem.chanName)
                            ? favoritesStorage.getFavorite(afterListItem.chanName, afterListItem.boardName,
                                    afterListItem.threadNumber)
                            : null;
            favoritesStorage.moveAfter(favoriteItem, afterFavoriteItem);
            invalidateItems(false, true);
        }
    }

    private boolean multipleFingersCountingTime = false;
    private long multipleFingersTime;
    private long multipleFingersStartTime;

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getActionMasked()) {
        case MotionEvent.ACTION_DOWN: {
            multipleFingersCountingTime = false;
            multipleFingersStartTime = 0L;
            multipleFingersTime = 0L;
            break;
        }
        case MotionEvent.ACTION_POINTER_DOWN: {
            if (!multipleFingersCountingTime) {
                multipleFingersCountingTime = true;
                multipleFingersStartTime = System.currentTimeMillis();
            }
            break;
        }
        case MotionEvent.ACTION_POINTER_UP: {
            if (event.getPointerCount() <= 2) {
                if (multipleFingersCountingTime) {
                    multipleFingersCountingTime = false;
                    multipleFingersTime += System.currentTimeMillis() - multipleFingersStartTime;
                }
            }
            break;
        }
        }
        return false;
    }

    public void performResume() {
        boolean mergeChans = Preferences.isMergeChans();
        if (this.mergeChans != mergeChans) {
            this.mergeChans = mergeChans;
            invalidateItems(true, true);
        }
    }

    public void invalidateItems(boolean pages, boolean favorites) {
        updateList(pages, favorites);
        notifyDataSetChanged();
    }

    @Override
    public void notifyDataSetChanged() {
        boolean focused = searchEdit.isFocused();
        if (focused) {
            focused = inputMethodManager != null && inputMethodManager.isActive(searchEdit);
        }
        super.notifyDataSetChanged();
        if (focused) {
            searchEdit.removeCallbacks(restoreSearchFocusRunnable);
            searchEdit.post(restoreSearchFocusRunnable);
        }
    }

    private final Runnable restoreSearchFocusRunnable = new Runnable() {
        @Override
        public void run() {
            searchEdit.requestFocus();
        }
    };

    @Override
    public int getEdgeEffectShift(boolean top) {
        int shift = listView.obtainEdgeEffectShift(top);
        return top ? shift + headerView.getPaddingTop() : shift;
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        ListItem listItem = getItem(position);
        if (listItem != null) {
            switch (listItem.type) {
            case ListItem.ITEM_PAGE:
            case ListItem.ITEM_FAVORITE: {
                boolean fromCache = listItem.type == ListItem.ITEM_PAGE;
                if (!listItem.isThreadItem()) {
                    callback.onSelectBoard(listItem.chanName, listItem.boardName, fromCache);
                } else {
                    callback.onSelectThread(listItem.chanName, listItem.boardName, listItem.threadNumber, null,
                            listItem.title, fromCache);
                }
                break;
            }
            case ListItem.ITEM_MENU: {
                callback.onSelectDrawerMenuItem(listItem.data);
                break;
            }
            case ListItem.ITEM_CHAN: {
                callback.onSelectChan(listItem.chanName);
                setChanSelectMode(false);
                if (C.API_LOLLIPOP) {
                    listView.getSelector().jumpToCurrentState();
                }
                break;
            }
            }
        }
    }

    private static final int MENU_COPY_LINK = 0;
    private static final int MENU_SHARE_LINK = 1;
    private static final int MENU_ADD_TO_FAVORITES = 2;
    private static final int MENU_REMOVE_FROM_FAVORITES = 3;
    private static final int MENU_RENAME = 4;

    @Override
    public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
        if (chanSelectMode) {
            return listView.startSorting(2, chans.size(), position);
        }
        final ListItem listItem = getItem(position);
        if (listItem != null) {
            if (listItem.type == ListItem.ITEM_FAVORITE && listItem.threadNumber != null
                    && FavoritesStorage.getInstance().canSortManually()) {
                long time = multipleFingersTime
                        + (multipleFingersCountingTime ? System.currentTimeMillis() - multipleFingersStartTime
                                : 0L);
                if (time >= ViewConfiguration.getLongPressTimeout() / 10) {
                    int start = position;
                    int end = position;
                    for (int i = position - 1;; i--) {
                        ListItem checkingListItem = getItem(i);
                        if (checkingListItem == null) {
                            break;
                        }
                        if (checkingListItem.type != ListItem.ITEM_FAVORITE
                                || !listItem.chanName.equals(checkingListItem.chanName)) {
                            start = i + 1;
                            break;
                        }
                    }
                    for (int i = position + 1;; i++) {
                        ListItem checkingListItem = getItem(i);
                        if (checkingListItem == null) {
                            break;
                        }
                        if (checkingListItem.type != ListItem.ITEM_FAVORITE
                                || !listItem.chanName.equals(checkingListItem.chanName)) {
                            end = i - 1;
                            break;
                        }
                    }
                    listView.startSorting(start, end, position);
                    return true;
                }
            }
            switch (listItem.type) {
            case ListItem.ITEM_PAGE:
            case ListItem.ITEM_FAVORITE: {
                DialogMenu dialogMenu = new DialogMenu(context, new DialogMenu.Callback() {
                    @Override
                    public void onItemClick(Context context, int id, Map<String, Object> extra) {
                        switch (id) {
                        case MENU_COPY_LINK:
                        case MENU_SHARE_LINK: {
                            ChanLocator locator = ChanLocator.get(listItem.chanName);
                            Uri uri = listItem.isThreadItem()
                                    ? locator.safe(true).createThreadUri(listItem.boardName, listItem.threadNumber)
                                    : locator.safe(true).createBoardUri(listItem.boardName, 0);
                            if (uri != null) {
                                switch (id) {
                                case MENU_COPY_LINK: {
                                    StringUtils.copyToClipboard(context, uri.toString());
                                    break;
                                }
                                case MENU_SHARE_LINK: {
                                    String subject = listItem.title;
                                    if (StringUtils.isEmptyOrWhitespace(subject)) {
                                        subject = uri.toString();
                                    }
                                    NavigationUtils.share(context, subject, null, uri);
                                    break;
                                }
                                }
                            }
                            break;
                        }
                        case MENU_ADD_TO_FAVORITES: {
                            if (listItem.isThreadItem()) {
                                FavoritesStorage.getInstance().add(listItem.chanName, listItem.boardName,
                                        listItem.threadNumber, listItem.title, 0);
                            } else {
                                FavoritesStorage.getInstance().add(listItem.chanName, listItem.boardName);
                            }
                            break;
                        }
                        case MENU_REMOVE_FROM_FAVORITES: {
                            FavoritesStorage.getInstance().remove(listItem.chanName, listItem.boardName,
                                    listItem.threadNumber);
                            break;
                        }
                        case MENU_RENAME: {
                            final EditText editText = new SafePasteEditText(context);
                            editText.setSingleLine(true);
                            editText.setText(listItem.title);
                            editText.setSelection(editText.length());
                            LinearLayout linearLayout = new LinearLayout(context);
                            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
                            linearLayout.addView(editText, LinearLayout.LayoutParams.MATCH_PARENT,
                                    LinearLayout.LayoutParams.WRAP_CONTENT);
                            int padding = context.getResources().getDimensionPixelSize(R.dimen.dialog_padding_view);
                            linearLayout.setPadding(padding, padding, padding, padding);
                            AlertDialog dialog = new AlertDialog.Builder(context).setView(linearLayout)
                                    .setTitle(R.string.action_rename)
                                    .setNegativeButton(android.R.string.cancel, null)
                                    .setPositiveButton(android.R.string.ok, (d, which) -> {
                                        String newTitle = editText.getText().toString();
                                        FavoritesStorage.getInstance().modifyTitle(listItem.chanName,
                                                listItem.boardName, listItem.threadNumber, newTitle, true);
                                    }).create();
                            dialog.getWindow()
                                    .setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
                            dialog.show();
                            break;
                        }
                        }
                    }
                });
                dialogMenu.addItem(MENU_COPY_LINK, R.string.action_copy_link);
                if (listItem.isThreadItem()) {
                    dialogMenu.addItem(MENU_SHARE_LINK, R.string.action_share_link);
                }
                if (listItem.type != ListItem.ITEM_FAVORITE && !FavoritesStorage.getInstance()
                        .hasFavorite(listItem.chanName, listItem.boardName, listItem.threadNumber)) {
                    dialogMenu.addItem(MENU_ADD_TO_FAVORITES, R.string.action_add_to_favorites);
                }
                if (listItem.type == ListItem.ITEM_FAVORITE) {
                    dialogMenu.addItem(MENU_REMOVE_FROM_FAVORITES, R.string.action_remove_from_favorites);
                    if (listItem.threadNumber != null) {
                        dialogMenu.addItem(MENU_RENAME, R.string.action_rename);
                    }
                }
                dialogMenu.show();
                return true;
            }
            }
        }
        return false;
    }

    @Override
    public void onDrawerSlide(View drawerView, float slideOffset) {
    }

    @Override
    public void onDrawerOpened(View drawerView) {
    }

    @Override
    public void onDrawerClosed(View drawerView) {
        hideKeyboard();
        setChanSelectMode(false);
    }

    @Override
    public void onDrawerStateChanged(int newState) {
    }

    private void clearTextAndHideKeyboard() {
        searchEdit.setText(null);
        hideKeyboard();
    }

    private void hideKeyboard() {
        searchEdit.clearFocus();
        if (inputMethodManager != null) {
            inputMethodManager.hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
        }
    }

    private static final Pattern PATTERN_NAVIGATION_BOARD_THREAD = Pattern.compile("([\\w_-]+) (\\d+)");
    private static final Pattern PATTERN_NAVIGATION_BOARD = Pattern.compile("/?([\\w_-]+)");
    private static final Pattern PATTERN_NAVIGATION_THREAD = Pattern.compile("#(\\d+)");

    @Override
    public void onClick(View v) {
        String text = searchEdit.getText().toString().trim();
        int number = -1;
        try {
            number = Integer.parseInt(text);
        } catch (NumberFormatException e) {
            // Not a number, ignore exception
        }
        if (number >= 0) {
            int result = callback.onEnterNumber(number);
            if (FlagUtils.get(result, RESULT_SUCCESS)) {
                clearTextAndHideKeyboard();
                return;
            }
            if (FlagUtils.get(result, RESULT_REMOVE_ERROR_MESSAGE)) {
                return;
            }
        } else {
            {
                String boardName = null;
                String threadNumber = null;
                Matcher matcher = PATTERN_NAVIGATION_BOARD_THREAD.matcher(text);
                if (matcher.matches()) {
                    boardName = matcher.group(1);
                    threadNumber = matcher.group(2);
                } else {
                    matcher = PATTERN_NAVIGATION_BOARD.matcher(text);
                    if (matcher.matches()) {
                        boardName = matcher.group(1);
                    } else {
                        matcher = PATTERN_NAVIGATION_THREAD.matcher(text);
                        if (matcher.matches()) {
                            threadNumber = matcher.group(1);
                        }
                    }
                }
                if (boardName != null || threadNumber != null) {
                    boolean success;
                    if (threadNumber == null) {
                        callback.onSelectBoard(chanName, boardName, false);
                        success = true;
                    } else {
                        success = callback.onSelectThread(chanName, boardName, threadNumber, null, null, false);
                    }
                    if (success) {
                        clearTextAndHideKeyboard();
                        return;
                    }
                }
            }
            Uri uri = Uri.parse(text);
            String chanName = ChanManager.getInstance().getChanNameByHost(uri.getAuthority());
            if (chanName != null) {
                boolean success = false;
                String boardName = null;
                String threadNumber = null;
                String postNumber = null;
                ChanLocator locator = ChanLocator.get(chanName);
                if (locator.safe(false).isThreadUri(uri)) {
                    boardName = locator.safe(false).getBoardName(uri);
                    threadNumber = locator.safe(false).getThreadNumber(uri);
                    postNumber = locator.safe(false).getPostNumber(uri);
                    success = true;
                } else if (locator.safe(false).isBoardUri(uri)) {
                    boardName = locator.safe(false).getBoardName(uri);
                    threadNumber = null;
                    postNumber = null;
                    success = true;
                }
                if (success) {
                    if (threadNumber == null) {
                        callback.onSelectBoard(chanName, boardName, false);
                    } else {
                        callback.onSelectThread(chanName, boardName, threadNumber, postNumber, null, false);
                    }
                    clearTextAndHideKeyboard();
                    return;
                }
            }
        }
        ToastUtils.show(context, R.string.message_enter_valid_data);
    }

    @Override
    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
        onClick(v);
        return true;
    }

    private void updateList(boolean pages, boolean favorites) {
        int mode = Preferences.getPagesListMode();
        if (pages) {
            this.pages.clear();
            if (mode != Preferences.PAGES_LIST_MODE_HIDE_PAGES) {
                updateListPages();
            }
        }
        if (favorites) {
            this.favorites.clear();
            updateListFavorites();
        }
        categories.clear();
        switch (mode) {
        case Preferences.PAGES_LIST_MODE_PAGES_FIRST: {
            categories.add(this.pages);
            categories.add(this.favorites);
            break;
        }
        case Preferences.PAGES_LIST_MODE_FAVORITES_FIRST: {
            categories.add(this.favorites);
            categories.add(this.pages);
            break;
        }
        case Preferences.PAGES_LIST_MODE_HIDE_PAGES: {
            categories.add(this.favorites);
            break;
        }
        }
        notifyDataSetChanged();
    }

    private void updateListPages() {
        boolean mergeChans = this.mergeChans;
        ArrayList<PageHolder> allPages = callback.getDrawerPageHolders();
        ArrayList<PageHolder> pages = new ArrayList<>();
        for (PageHolder pageHolder : allPages) {
            boolean isThreads = pageHolder.content == PageHolder.Content.THREADS;
            boolean isPosts = pageHolder.content == PageHolder.Content.POSTS;
            if (isThreads || isPosts) {
                if (mergeChans || pageHolder.chanName.equals(chanName)) {
                    if (pageHolder.threadNumber != null || !ChanConfiguration.get(pageHolder.chanName)
                            .getOption(ChanConfiguration.OPTION_SINGLE_BOARD_MODE)) {
                        pages.add(pageHolder);
                    }
                }
            }
        }
        if (pages.size() > 0) {
            Collections.sort(pages, DATE_COMPARATOR);
            this.pages.add(new ListItem(ListItem.ITEM_HEADER, null, null, null,
                    context.getString(R.string.text_open_pages), HEADER_ACTION_CLOSE_ALL,
                    ResourceUtils.getResourceId(context, R.attr.buttonCancel, 0)));
            for (PageHolder pageHolder : pages) {
                Drawable drawable = chanIcons.get(pageHolder.chanName);
                if (pageHolder.threadNumber != null) {
                    this.pages.add(new ListItem(ListItem.ITEM_PAGE, pageHolder.chanName, pageHolder.boardName,
                            pageHolder.threadNumber, pageHolder.threadTitle, 0, drawable));
                } else {
                    this.pages.add(new ListItem(ListItem.ITEM_PAGE, pageHolder.chanName, pageHolder.boardName, null,
                            ChanConfiguration.get(pageHolder.chanName).getBoardTitle(pageHolder.boardName), 0,
                            drawable));
                }
            }
        }
    }

    private static final Comparator<PageHolder> DATE_COMPARATOR = (lhs,
            rhs) -> (int) (rhs.creationTime - lhs.creationTime);

    private void updateListFavorites() {
        boolean mergeChans = this.mergeChans;
        FavoritesStorage favoritesStorage = FavoritesStorage.getInstance();
        ArrayList<FavoritesStorage.FavoriteItem> favoriteBoards = favoritesStorage
                .getBoards(mergeChans ? null : chanName);
        ArrayList<FavoritesStorage.FavoriteItem> favoriteThreads = favoritesStorage
                .getThreads(mergeChans ? null : chanName);
        boolean addHeader = true;
        Collection<String> chanNames = ChanManager.getInstance().getAvailableChanNames();
        for (int i = 0; i < favoriteThreads.size(); i++) {
            FavoritesStorage.FavoriteItem favoriteItem = favoriteThreads.get(i);
            if (!chanNames.contains(favoriteItem.chanName)) {
                continue;
            }
            if (mergeChans || favoriteItem.chanName.equals(chanName)) {
                if (addHeader) {
                    if (watcherSupportSet.contains(favoriteItem.chanName)
                            || mergeChans && !watcherSupportSet.isEmpty()) {
                        favorites.add(new ListItem(ListItem.ITEM_HEADER, null, null, null,
                                context.getString(R.string.text_favorite_threads), HEADER_ACTION_REFRESH_WATCHER,
                                ResourceUtils.getResourceId(context, R.attr.buttonRefresh, 0)));
                    } else {
                        favorites.add(new ListItem(ListItem.ITEM_HEADER, null, null, null,
                                context.getString(R.string.text_favorite_threads)));
                    }
                    addHeader = false;
                }
                ListItem listItem = new ListItem(ListItem.ITEM_FAVORITE, favoriteItem.chanName,
                        favoriteItem.boardName, favoriteItem.threadNumber, favoriteItem.title, 0,
                        chanIcons.get(favoriteItem.chanName));
                if (watcherSupportSet.contains(favoriteItem.chanName)) {
                    WatcherService.TemporalCountData temporalCountData = watcherServiceClient
                            .countNewPosts(favoriteItem);
                    listItem.watcherPostsCountDifference = temporalCountData.postsCountDifference;
                    listItem.watcherHasNewPosts = temporalCountData.hasNewPosts;
                    listItem.watcherIsError = temporalCountData.isError;
                }
                favorites.add(listItem);
            }
        }
        addHeader = true;
        for (int i = 0; i < favoriteBoards.size(); i++) {
            FavoritesStorage.FavoriteItem favoriteItem = favoriteBoards.get(i);
            if (!chanNames.contains(favoriteItem.chanName)) {
                continue;
            }
            if (mergeChans || favoriteItem.chanName.equals(chanName)) {
                if (addHeader) {
                    favorites.add(new ListItem(ListItem.ITEM_HEADER, null, null, null,
                            context.getString(R.string.text_favorite_boards)));
                    addHeader = false;
                }
                favorites.add(new ListItem(ListItem.ITEM_FAVORITE, favoriteItem.chanName, favoriteItem.boardName,
                        null, ChanConfiguration.get(favoriteItem.chanName).getBoardTitle(favoriteItem.boardName), 0,
                        chanIcons.get(favoriteItem.chanName)));
            }
        }
    }

    private static class ListItem {
        public static final int ITEM_HEADER = 1;
        public static final int ITEM_PAGE = 2;
        public static final int ITEM_FAVORITE = 3;
        public static final int ITEM_MENU = 4;
        public static final int ITEM_CHAN = 5;
        public static final int ITEM_DIVIDER = 6;

        public final int type, data, resourceId;
        public final String chanName, boardName, threadNumber, title;
        public final Drawable resource;

        private int watcherPostsCountDifference = 0;
        private boolean watcherHasNewPosts = false;
        private boolean watcherIsError = false;

        public ListItem(int type, String chanName, String boardName, String threadNumber, String title) {
            this(type, chanName, boardName, threadNumber, title, 0, 0);
        }

        public ListItem(int type, String chanName, String boardName, String threadNumber, String title, int data,
                int resourceId) {
            this.type = type;
            this.data = data;
            this.resourceId = resourceId;
            this.resource = null;
            this.chanName = chanName;
            this.boardName = boardName;
            this.threadNumber = threadNumber;
            this.title = title;
        }

        public ListItem(int type, String chanName, String boardName, String threadNumber, String title, int data,
                Drawable resource) {
            this.type = type;
            this.data = data;
            this.resourceId = 0;
            this.resource = resource;
            this.chanName = chanName;
            this.boardName = boardName;
            this.threadNumber = threadNumber;
            this.title = title;
        }

        public ListItem(int type, int data, int resourceId, String title) {
            this.type = type;
            this.data = data;
            this.resourceId = resourceId;
            this.resource = null;
            this.chanName = null;
            this.boardName = null;
            this.threadNumber = null;
            this.title = title;
        }

        public boolean isThreadItem() {
            return threadNumber != null;
        }

        public boolean compare(String chanName, String boardName, String threadNumber) {
            return StringUtils.equals(this.chanName, chanName) && StringUtils.equals(this.boardName, boardName)
                    && StringUtils.equals(this.threadNumber, threadNumber);
        }
    }

    private final View.OnClickListener closeButtonListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ListItem listItem = getItem(v);
            if (listItem != null && listItem.type == ListItem.ITEM_PAGE) {
                boolean result = callback.onClosePage(listItem.chanName, listItem.boardName, listItem.threadNumber);
                if (result) {
                    // size == 2 means only one item in list + header
                    if (pages.size() == 2) {
                        pages.clear();
                    } else {
                        pages.remove(listItem);
                    }
                    notifyDataSetChanged();
                }
            }
        }
    };

    private static final int HEADER_ACTION_CLOSE_ALL = 0;
    private static final int HEADER_ACTION_REFRESH_WATCHER = 1;

    private final View.OnClickListener headerButtonListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            ListItem listItem = getItem(v);
            if (listItem != null && listItem.type == ListItem.ITEM_HEADER) {
                switch (listItem.data) {
                case HEADER_ACTION_CLOSE_ALL: {
                    callback.onCloseAllPages();
                    break;
                }
                case HEADER_ACTION_REFRESH_WATCHER: {
                    watcherServiceClient.update();
                    break;
                }
                }
            }
        }
    };

    private static final int TYPE_VIEW = 0;
    private static final int TYPE_VIEW_ICON = 1;
    private static final int TYPE_VIEW_WATCHER = 2;
    private static final int TYPE_VIEW_WATCHER_ICON = 3;
    private static final int TYPE_VIEW_CLOSEABLE = 4;
    private static final int TYPE_VIEW_CLOSEABLE_ICON = 5;
    private static final int TYPE_HEADER = 6;
    private static final int TYPE_HEADER_BUTTON = 7;
    private static final int TYPE_DIVIDER = 8;

    @Override
    public int getViewTypeCount() {
        return 9;
    }

    @Override
    public int getItemViewType(int position) {
        ListItem listItem = getItem(position);
        if (listItem != null) {
            switch (listItem.type) {
            case ListItem.ITEM_HEADER: {
                return listItem.resourceId != 0 || listItem.resource != null ? TYPE_HEADER_BUTTON : TYPE_HEADER;
            }
            case ListItem.ITEM_PAGE: {
                return mergeChans && C.API_LOLLIPOP ? TYPE_VIEW_CLOSEABLE_ICON : TYPE_VIEW_CLOSEABLE;
            }
            case ListItem.ITEM_FAVORITE: {
                if (listItem.threadNumber != null) {
                    boolean watcherSupported = watcherSupportSet.contains(listItem.chanName);
                    if (mergeChans && C.API_LOLLIPOP) {
                        return watcherSupported ? TYPE_VIEW_WATCHER_ICON : TYPE_VIEW_ICON;
                    } else {
                        return watcherSupported ? TYPE_VIEW_WATCHER : TYPE_VIEW;
                    }
                } else {
                    return mergeChans && C.API_LOLLIPOP ? TYPE_VIEW_ICON : TYPE_VIEW;
                }
            }
            case ListItem.ITEM_MENU: {
                return TYPE_VIEW_ICON;
            }
            case ListItem.ITEM_CHAN: {
                return C.API_LOLLIPOP ? TYPE_VIEW_ICON : TYPE_VIEW;
            }
            case ListItem.ITEM_DIVIDER: {
                return TYPE_DIVIDER;
            }
            }
        }
        return IGNORE_ITEM_VIEW_TYPE;
    }

    @Override
    public int getCount() {
        int count = 1;
        if (chanSelectMode) {
            count += chans.size();
        } else {
            for (int i = 0; i < categories.size(); i++) {
                count += categories.get(i).size();
            }
            count += menu.size();
        }
        return count;
    }

    @Override
    public ListItem getItem(int position) {
        position--;
        if (position >= 0) {
            if (chanSelectMode) {
                if (position < chans.size()) {
                    return chans.get(position);
                }
            } else {
                for (int i = 0; i < categories.size(); i++) {
                    ArrayList<ListItem> listItems = categories.get(i);
                    if (position < listItems.size()) {
                        return listItems.get(position);
                    }
                    position -= listItems.size();
                    if (position < 0) {
                        return null;
                    }
                }
                if (position < menu.size()) {
                    return menu.get(position);
                }
            }
        }
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public boolean isEnabled(int position) {
        ListItem listItem = getItem(position);
        if (listItem != null) {
            int type = getItem(position).type;
            return type == ListItem.ITEM_PAGE || type == ListItem.ITEM_FAVORITE || type == ListItem.ITEM_MENU
                    || type == ListItem.ITEM_CHAN;
        }
        return false;
    }

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

    private View makeSimpleDivider() {
        float density = ResourceUtils.obtainDensity(context);
        if (C.API_LOLLIPOP) {
            FrameLayout frameLayout = new FrameLayout(context);
            View view = new View(context);
            view.setBackgroundResource(ResourceUtils.getResourceId(context, android.R.attr.listDivider, 0));
            frameLayout.addView(view, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
            frameLayout.setPadding(0, (int) (8f * density), 0, (int) (8f * density));
            return frameLayout;
        } else {
            View view = new View(context);
            int[] attrs = { android.R.attr.listSeparatorTextViewStyle };
            TypedArray typedArray = context.obtainStyledAttributes(attrs);
            int style = typedArray.getResourceId(0, 0);
            typedArray.recycle();
            if (style != 0) {
                typedArray = context.obtainStyledAttributes(style, new int[] { android.R.attr.background });
                Drawable drawable = typedArray.getDrawable(0);
                typedArray.recycle();
                if (drawable != null) {
                    view.setBackgroundColor(GraphicsUtils.getDrawableColor(context, drawable, Gravity.BOTTOM));
                }
            }
            view.setMinimumHeight((int) (2f * density));
            return view;
        }
    }

    private TextView makeCommonTextView(boolean header) {
        TextView textView = new TextView(context, null,
                C.API_LOLLIPOP ? android.R.attr.textAppearanceListItem : android.R.attr.textAppearance);
        textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, C.API_LOLLIPOP ? 14f : 16f);
        textView.setGravity(Gravity.CENTER_VERTICAL);
        textView.setEllipsize(TextUtils.TruncateAt.END);
        textView.setSingleLine(true);
        if (C.API_LOLLIPOP) {
            textView.setTypeface(GraphicsUtils.TYPEFACE_MEDIUM);
            int color = textView.getTextColors().getDefaultColor();
            if (header) {
                color &= 0x5effffff;
            } else {
                color &= 0xddffffff;
            }
            textView.setTextColor(color);
        }
        return textView;
    }

    private View makeView(boolean icon, boolean watcher, boolean closeable, float density) {
        int size = (int) (48f * density);
        LinearLayout linearLayout = new LinearLayout(context);
        linearLayout.setOrientation(LinearLayout.HORIZONTAL);
        linearLayout.setGravity(Gravity.CENTER_VERTICAL);
        ImageView iconView = null;
        if (icon) {
            iconView = new ImageView(context);
            iconView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
            linearLayout.addView(iconView, (int) (24f * density), size);
        }
        TextView textView = makeCommonTextView(false);
        linearLayout.addView(textView, new LinearLayout.LayoutParams(0, size, 1));
        WatcherView watcherView = null;
        if (watcher) {
            watcherView = new WatcherView(context);
            linearLayout.addView(watcherView, size, size);
        }
        ImageView closeView = null;
        if (!watcher && closeable) {
            closeView = new ImageView(context);
            closeView.setScaleType(ImageView.ScaleType.CENTER);
            closeView.setImageResource(ResourceUtils.getResourceId(context, R.attr.buttonCancel, 0));
            closeView.setBackgroundResource(ResourceUtils.getResourceId(context,
                    android.R.attr.borderlessButtonStyle, android.R.attr.background, 0));
            linearLayout.addView(closeView, size, size);
            closeView.setOnClickListener(closeButtonListener);
        }
        ViewHolder holder = new ViewHolder();
        holder.icon = iconView;
        holder.text = textView;
        holder.extra = watcherView != null ? watcherView : closeView;
        linearLayout.setTag(holder);
        int layoutLeftDp = 0;
        int layoutRightDp = 0;
        int textLeftDp;
        int textRightDp;
        if (C.API_LOLLIPOP) {
            textLeftDp = 16;
            textRightDp = 16;
            if (icon) {
                layoutLeftDp = 16;
                textLeftDp = 32;
            }
            if (watcher || closeable) {
                layoutRightDp = 4;
                textRightDp = 8;
            }
        } else {
            textLeftDp = 8;
            textRightDp = 8;
            if (icon) {
                layoutLeftDp = 8;
                textLeftDp = 6;
                textView.setAllCaps(true);
            }
            if (watcher || closeable) {
                layoutRightDp = 0;
                textRightDp = 0;
            }
        }
        linearLayout.setPadding((int) (layoutLeftDp * density), 0, (int) (layoutRightDp * density), 0);
        textView.setPadding((int) (textLeftDp * density), 0, (int) (textRightDp * density), 0);
        return linearLayout;
    }

    private View makeHeader(ViewGroup parent, boolean button, float density) {
        if (C.API_LOLLIPOP) {
            LinearLayout linearLayout = new LinearLayout(context);
            linearLayout.setOrientation(LinearLayout.VERTICAL);
            View divider = makeSimpleDivider();
            int paddingTop = divider.getPaddingBottom();
            divider.setPadding(divider.getPaddingLeft(), divider.getPaddingTop(), divider.getPaddingRight(), 0);
            linearLayout.addView(divider, LinearLayout.LayoutParams.MATCH_PARENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT);
            LinearLayout linearLayout2 = new LinearLayout(context);
            linearLayout2.setOrientation(LinearLayout.HORIZONTAL);
            linearLayout.addView(linearLayout2, LinearLayout.LayoutParams.MATCH_PARENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT);
            TextView textView = makeCommonTextView(true);
            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, (int) (32f * density), 1);
            layoutParams.setMargins((int) (16f * density), paddingTop, (int) (16f * density), (int) (8f * density));
            linearLayout2.addView(textView, layoutParams);
            ViewHolder holder = new ViewHolder();
            holder.text = textView;
            if (button) {
                ImageView imageView = new ImageView(context);
                imageView.setScaleType(ImageView.ScaleType.CENTER);
                imageView.setBackgroundResource(ResourceUtils.getResourceId(context,
                        android.R.attr.borderlessButtonStyle, android.R.attr.background, 0));
                imageView.setOnClickListener(headerButtonListener);
                imageView.setImageAlpha(0x5e);
                int size = (int) (48f * density);
                layoutParams = new LinearLayout.LayoutParams(size, size);
                layoutParams.rightMargin = (int) (4f * density);
                linearLayout2.addView(imageView, layoutParams);
                holder.extra = imageView;
                holder.icon = imageView;
            }
            linearLayout.setTag(holder);
            return linearLayout;
        } else {
            View view = LayoutInflater.from(context)
                    .inflate(ResourceUtils.getResourceId(context, android.R.attr.preferenceCategoryStyle,
                            android.R.attr.layout, android.R.layout.preference_category), parent, false);
            ViewHolder holder = new ViewHolder();
            holder.text = (TextView) view.findViewById(android.R.id.title);
            if (button) {
                int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
                view.measure(measureSpec, measureSpec);
                int size = view.getMeasuredHeight();
                if (size == 0) {
                    size = (int) (32f * density);
                }
                FrameLayout frameLayout = new FrameLayout(context);
                frameLayout.addView(view);
                view = frameLayout;
                ImageView imageView = new ImageView(context);
                imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
                int padding = (int) (4f * density);
                imageView.setPadding(padding, padding, padding, padding);
                frameLayout.addView(imageView,
                        new FrameLayout.LayoutParams((int) (48f * density), size, Gravity.END));
                View buttonView = new View(context);
                buttonView.setBackgroundResource(
                        ResourceUtils.getResourceId(context, android.R.attr.selectableItemBackground, 0));
                buttonView.setOnClickListener(headerButtonListener);
                frameLayout.addView(buttonView, FrameLayout.LayoutParams.MATCH_PARENT,
                        FrameLayout.LayoutParams.MATCH_PARENT);
                holder.extra = buttonView;
                holder.icon = imageView;
            }
            view.setTag(holder);
            return view;
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ListItem listItem = getItem(position);
        if (listItem == null) {
            switch (position) {
            case 0: {
                return headerView;
            }
            }
            throw new NullPointerException();
        }
        if (convertView == null) {
            float density = ResourceUtils.obtainDensity(context);
            int viewType = getItemViewType(position);
            switch (viewType) {
            case TYPE_VIEW:
            case TYPE_VIEW_ICON:
            case TYPE_VIEW_WATCHER:
            case TYPE_VIEW_WATCHER_ICON:
            case TYPE_VIEW_CLOSEABLE:
            case TYPE_VIEW_CLOSEABLE_ICON: {
                boolean icon = viewType == TYPE_VIEW_ICON || viewType == TYPE_VIEW_WATCHER_ICON
                        || viewType == TYPE_VIEW_CLOSEABLE_ICON;
                boolean watcher = viewType == TYPE_VIEW_WATCHER || viewType == TYPE_VIEW_WATCHER_ICON;
                boolean closeable = viewType == TYPE_VIEW_CLOSEABLE || viewType == TYPE_VIEW_CLOSEABLE_ICON;
                convertView = makeView(icon, watcher, closeable, density);
                break;
            }
            case TYPE_HEADER:
            case TYPE_HEADER_BUTTON: {
                convertView = makeHeader(parent, viewType == TYPE_HEADER_BUTTON, density);
                break;
            }
            case TYPE_DIVIDER: {
                convertView = makeSimpleDivider();
                break;
            }
            }
        }
        ViewHolder holder = (ViewHolder) convertView.getTag();
        if (holder != null) {
            holder.listItem = listItem;
        }
        switch (listItem.type) {
        case ListItem.ITEM_PAGE:
        case ListItem.ITEM_FAVORITE: {
            String text;
            if (listItem.isThreadItem()) {
                if (!StringUtils.isEmptyOrWhitespace(listItem.title)) {
                    text = listItem.title;
                } else {
                    text = StringUtils.formatThreadTitle(listItem.chanName, listItem.boardName,
                            listItem.threadNumber);
                }
            } else {
                text = StringUtils.formatBoardTitle(listItem.chanName, listItem.boardName, listItem.title);
            }
            holder.text.setText(text);
            if (listItem.type == ListItem.ITEM_FAVORITE && listItem.isThreadItem()
                    && watcherSupportSet.contains(listItem.chanName)) {
                WatcherService.WatcherItem watcherItem = watcherServiceClient.getItem(listItem.chanName,
                        listItem.boardName, listItem.threadNumber);
                updateWatcherItem(holder,
                        watcherItem != null ? watcherItem.getLastState() : WatcherService.State.DISABLED);
            }
            break;
        }
        case ListItem.ITEM_HEADER:
        case ListItem.ITEM_MENU:
        case ListItem.ITEM_CHAN: {
            holder.text.setText(listItem.title);
            break;
        }
        }
        if (holder != null && holder.icon != null) {
            if (listItem.resourceId != 0) {
                holder.icon.setImageResource(listItem.resourceId);
            } else if (listItem.resource != null) {
                holder.icon.setImageDrawable(listItem.resource);
            } else {
                holder.icon.setImageDrawable(null);
            }
        }
        return convertView;
    }

    private class ViewHolder {
        public ListItem listItem;
        public TextView text;
        public ImageView icon;
        public View extra;
    }

    private ListItem getItem(View view) {
        view = ListViewUtils.getRootViewInList(view);
        ViewHolder holder = (ViewHolder) view.getTag();
        return holder.listItem;
    }

    private void updateWatcherItem(ViewHolder holder, WatcherService.State state) {
        WatcherView watcherView = (WatcherView) holder.extra;
        watcherView.setPostsCountDifference(holder.listItem.watcherPostsCountDifference,
                holder.listItem.watcherHasNewPosts, holder.listItem.watcherIsError);
        // Null state means state not changed
        if (state != null) {
            watcherView.setWatcherState(state);
        }
    }

    private WatcherDrawableColorSet watcherDrawableColorSet;

    private class WatcherDrawableColorSet {
        public final int enabledColor;
        public final int unavailableColor;
        public final int disabledColor;

        public WatcherDrawableColorSet(int enabledColor, int unavailableColor, int disabledColor) {
            this.enabledColor = enabledColor;
            this.unavailableColor = unavailableColor;
            this.disabledColor = disabledColor;
        }
    }

    private class WatcherView extends FrameLayout implements View.OnClickListener {
        private final ProgressBar progressBar;

        private String text = "";
        private boolean hasNew = false;
        private int color;

        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        public WatcherView(Context context) {
            super(context);
            setBackgroundResource(ResourceUtils.getResourceId(context, android.R.attr.selectableItemBackground, 0));
            progressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleSmall);
            if (C.API_LOLLIPOP) {
                progressBar.getIndeterminateDrawable().setTint(Color.WHITE);
            }
            addView(progressBar, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
                    FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.CENTER));
            setOnClickListener(this);
            setWatcherState(WatcherService.State.DISABLED);
            setPostsCountDifference(0, false, false);
        }

        private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG);
        private final RectF rectF = new RectF();
        private final Rect rect = new Rect();

        @Override
        public void draw(Canvas canvas) {
            float density = ResourceUtils.obtainDensity(context);
            if (C.API_LOLLIPOP) {
                int paddingHorizontal = (int) (8f * density);
                int paddingVertical = (int) (12f * density);
                rectF.set(paddingHorizontal, paddingVertical, getWidth() - paddingHorizontal,
                        getHeight() - paddingVertical);
            } else {
                int padding = (int) (8f * density);
                rectF.set(padding, padding, getWidth() - padding, getHeight() - padding);
            }
            int cornerRadius = C.API_LOLLIPOP ? (int) density : (int) (4f * density);
            paint.setColor(color);
            canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, paint);
            canvas.save();
            canvas.clipRect(rectF);
            super.draw(canvas);
            if (progressBar.getVisibility() != View.VISIBLE) {
                float fontSize = C.API_LOLLIPOP ? 12f : 16f;
                paint.setColor(Color.WHITE);
                if (!hasNew) {
                    paint.setAlpha(0x99);
                }
                paint.setTextSize(fontSize * getResources().getDisplayMetrics().scaledDensity);
                paint.setTextAlign(Paint.Align.CENTER);
                paint.getTextBounds(text, 0, text.length(), rect);
                canvas.drawText(text, getWidth() / 2f, (getHeight() + rect.height()) / 2f, paint);
            } else if (hasNew) {
                paint.setColor(Color.WHITE);
                canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, (int) (4f * density), paint);
            }
            canvas.restore();
        }

        public void setPostsCountDifference(int postsCountDifference, boolean hasNew, boolean error) {
            String text;
            if (postsCountDifference == WatcherService.POSTS_COUNT_DIFFERENCE_DELETED) {
                text = "X";
            } else {
                if (Math.abs(postsCountDifference) >= 1000) {
                    text = Integer.toString(postsCountDifference / 1000) + "K+";
                } else {
                    text = Integer.toString(postsCountDifference);
                }
                if (error) {
                    text += "?";
                }
            }
            this.text = text;
            this.hasNew = hasNew;
            invalidate();
        }

        public void setWatcherState(WatcherService.State state) {
            if (watcherDrawableColorSet == null) {
                int enabledColor = ResourceUtils.getColor(unstyledContext, R.attr.colorAccentSupport);
                int disabledColor = 0xff666666;
                int unavailableColor = GraphicsUtils.mixColors(disabledColor, enabledColor & 0x7fffffff);
                watcherDrawableColorSet = new WatcherDrawableColorSet(enabledColor, unavailableColor,
                        disabledColor);
            }
            switch (state) {
            case DISABLED:
            case ENABLED:
            case UNAVAILABLE: {
                progressBar.setVisibility(View.GONE);
                break;
            }
            case BUSY: {
                progressBar.setVisibility(View.VISIBLE);
                break;
            }
            }
            switch (state) {
            case DISABLED: {
                color = watcherDrawableColorSet.disabledColor;
                break;
            }
            case ENABLED:
            case BUSY: {
                color = watcherDrawableColorSet.enabledColor;
                break;
            }
            case UNAVAILABLE: {
                color = watcherDrawableColorSet.unavailableColor;
                break;
            }
            }
            invalidate();
        }

        @Override
        public void onClick(View v) {
            ListItem listItem = getItem(this);
            FavoritesStorage.getInstance().toggleWatcher(listItem.chanName, listItem.boardName,
                    listItem.threadNumber);
        }
    }

    @Override
    public void onWatcherUpdate(WatcherService.WatcherItem watcherItem, WatcherService.State state) {
        if ((mergeChans || watcherItem.chanName.equals(chanName))
                && watcherSupportSet.contains(watcherItem.chanName)) {
            ListItem targetItem = null;
            for (ListItem listItem : favorites) {
                if (listItem.compare(watcherItem.chanName, watcherItem.boardName, watcherItem.threadNumber)) {
                    targetItem = listItem;
                    break;
                }
            }
            if (targetItem != null) {
                targetItem.watcherPostsCountDifference = watcherItem.getPostsCountDifference();
                targetItem.watcherHasNewPosts = watcherItem.hasNewPosts();
                targetItem.watcherIsError = watcherItem.isError();
                if (!listView.isSorting()) {
                    for (int i = 0, count = listView.getChildCount(); i < count; i++) {
                        View view = listView.getChildAt(i);
                        ViewHolder holder = (ViewHolder) view.getTag();
                        if (holder != null && targetItem == holder.listItem) {
                            updateWatcherItem(holder, state);
                            break;
                        }
                    }
                }
            }
        }
    }

    private class ListViewScrollFixListener implements ListView.OnScrollListener {
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        }

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if (scrollState == SCROLL_STATE_TOUCH_SCROLL) {
                View focusView = listView.getFocusedChild();
                if (focusView != null) {
                    focusView.clearFocus();
                    hideKeyboard();
                }
            }
        }
    }
}