@EApplication
public class MonodictApp extends Application {

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

    private static final EventBus eventBus = new EventBus();

    public static final EventBus getEventBus() {
        return eventBus;
    }

    @Override
    public void onCreate() {
        super.onCreate();
//        setLocale(Locale.JAPANESE);
    }

    public static final PackageInfo getPackageInfo(Context context) {
        String packageName = context.getPackageName();
        try {
            return context.getPackageManager().getPackageInfo(packageName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    private void setLocale(Locale locale) {
        Resources resources = getResources();
        Configuration conf = resources.getConfiguration();
        conf.locale = locale;
        resources.updateConfiguration(conf, resources.getDisplayMetrics());
    }
} @EBean
public class ActivityHelper {

    //private static final int REQUEST_CODE = 20100;

    @RootContext
    Activity activity;

    @DimensionRes
    float spaceSuperRelax;

    @Pref
    Preferences_ preferences;

    @Bean
    Dictionaries dictionaries;

    private ProgressDialog progressDialog = null;

    /**
     * http://stackoverflow.com/questions/6547969/android-refresh-current-activity
     */
    public void restartActivity() {
        Intent intent = new Intent(activity, activity.getClass());
        intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        activity.finish();
        activity.startActivity(intent);
    }

    public void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
        }
    }

    public void showToast(@StringRes int resId) {
        String message = activity.getResources().getString(resId);
        showToast(message);
    }

    @UiThread
    public void showToast(String message) {
        Toast toast = Toast.makeText(activity.getApplicationContext(), message, Toast.LENGTH_SHORT);
        toast.setGravity(Gravity.BOTTOM, 0, 0);
        toast.show();
    }

    public void showToastLong(@StringRes int resId) {
        showToastLong(activity.getResources().getString(resId));
    }

    @UiThread
    public void showToastLong(String message) {
        Toast toast = Toast.makeText(activity.getApplicationContext(), message, Toast.LENGTH_LONG);
        toast.setGravity(Gravity.CENTER, 0, 0);
        toast.show();
    }

    public void showError(Exception e) {
        Log.d("ERROR", e.toString());
        showToastLong(e.toString());
    }

    /**
     * Build dialog with user input
     *
     * @param text
     * @param callback
     * @return MaterialDialog.Builder
     */
    public MaterialDialog.Builder buildInputDialog(CharSequence text, MaterialDialog.ButtonCallback callback) {
        final EditText editText = new EditText(activity);
        if (text != null) {
            editText.setText(text);
        }
        editText.setId(android.R.id.text1);
        return new MaterialDialog.Builder(activity)
                .customView(editText, false)
                .positiveText(android.R.string.ok)
                .negativeText(android.R.string.cancel)
                .callback(callback);
    }

    /**
     * Build confirm dialog
     *
     * @param callback
     * @return MaterialDialog.Builder
     */
    public MaterialDialog.Builder buildConfirmDialog(MaterialDialog.ButtonCallback callback) {
        return new MaterialDialog.Builder(activity)
                .positiveText(android.R.string.ok)
                .callback(callback)
                .negativeText(android.R.string.cancel);
    }

    public MaterialDialog.Builder buildNoticeDialog(CharSequence text) {
        TextView textView = new TextView(activity);
        textView.setAutoLinkMask(Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES);
        textView.setText(text);
        return new MaterialDialog.Builder(activity)
                .customView(textView, true)
                .positiveText(android.R.string.ok);
    }

    @Nullable
    public PopupMenu popupSearchEngines(@NonNull final CharSequence query, @NonNull View anchor) {
        final SearchEngines searchEngines;
        try {
            InputStream inputStream = activity.getAssets().open("search_engines.json");
            searchEngines = (new Gson()).fromJson(
                    CharStreams.toString(new InputStreamReader(inputStream)),
                    SearchEngines.class);
        } catch (MalformedJsonException e) {
            showError(e);
            return null;
        } catch (IOException e) {
            showError(e);
            return null;
        }
        PopupMenu popupMenu = new PopupMenu(activity, anchor);
        for (SearchEnginesItem item : searchEngines.getItems()) {
            popupMenu.getMenu().add(item.getName());
        }
        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
            @Override
            public boolean onMenuItemClick(MenuItem item) {
                for (SearchEnginesItem searchEngine : searchEngines.getItems()) {
                    if (item.getTitle().equals(searchEngine.getName())) {
                        BrowserActivity_.intent(activity)
                                .extraUrlOrKeywords(String.format(searchEngine.getUrl(), query))
                                .start();
                    }
                }
                return false;
            }
        });
        return popupMenu;
    }

    public void onDuplicatedCardFound(final Card duplicateCard, final String newTranslate, final String newDictionary) {
        int box = duplicateCard.getBox();
        Resources resources = activity.getResources();
        String boxName = String.format("BOX%d", box);
        if (box <= 1) {
            boxName = "INBOX";
        }

        String message = resources.getString(
                R.string.message_item_already_registered_and_confirm,
                duplicateCard.getDisplay(),
                boxName);
        final DatabaseHelper databaseHelper = DatabaseHelper_.getInstance_(activity);

        buildConfirmDialog(new MaterialDialog.ButtonCallback() {
            @Override
            public void onPositive(MaterialDialog materialDialog) {
                duplicateCard.setBox(1);
                duplicateCard.setTranslate(newTranslate);
                duplicateCard.setDictionary(newDictionary);
                duplicateCard.notifyChangeRequest(Model.ModelChangeRequestEvent.TYPE_UPDATE);
            }
        }).title(duplicateCard.getDisplay()).content(message).show();
    }

    /**
     * Show ProgressDialog
     */
    @UiThread
    public void showProgressDialog(String message) {
        if (progressDialog != null) {
            return;
        }
        progressDialog = new ProgressDialog(activity);
        progressDialog.setIndeterminate(true);
        progressDialog.setCancelable(false);
        progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        progressDialog.setMessage(message);
        progressDialog.show();
    }

    public void showProgressDialog(@StringRes int resId) {
        showProgressDialog(activity.getResources().getString(resId));
    }

    /**
     * http://stackoverflow.com/questions/19538282/view-not-attached-to-window-manager-dialog-dismiss
     */
    @UiThread
    public void hideProgressDialog() {
        if (progressDialog != null) {
            if (!activity.isFinishing()) {
                try {
                    progressDialog.dismiss();
                    progressDialog = null;
                } catch (Exception e) {
                    // pass
                }
            }
        }
    }

    /**
     * Show progressBar
     *
     * @see #hideProgressBar
     */
    @UiThread
    public void showProgressBar() {
        ProgressBar progressBar = (ProgressBar) activity.findViewById(R.id.progress_bar);
        if (progressBar != null) {
            progressBar.setVisibility(View.VISIBLE);
        }
    }

    /**
     * Hide progressBar
     *
     * @see #showProgressBar
     */
    @UiThread
    public void hideProgressBar() {
        ProgressBar progressBar = (ProgressBar) activity.findViewById(R.id.progress_bar);
        if (progressBar != null) {
            progressBar.setVisibility(View.GONE);
        }
    }

    public void hideProgressBar(int delay) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                hideProgressBar();
            }
        }, delay);
    }

    public Spanned getHtmlFromRaw(@RawRes int resId) {
        return Html.fromHtml(getStringFromRaw(resId));
    }

    public String getStringFromRaw(@RawRes int resId) {
        InputStream stream = activity.getResources().openRawResource(resId);
        String text;
        try {
            text = CharStreams.toString(new InputStreamReader(stream));
        } catch (IOException e) {
            return "";
        }
        return text;
    }

    public void searchOnMainActivity(String query) {
        Intent intent = MainActivity_.intent(activity).get();
        intent.putExtra(SearchManager.QUERY, query);
        intent.setAction(Intent.ACTION_SEARCH);
        activity.startActivity(intent);
    }

    public void clear() {
        if (progressDialog != null) {
            progressDialog.dismiss();
            progressDialog = null;
        }
    }

    @UiThread
    public void setListPosition(int position) {
        ((ListActivity) activity).getListView().setSelection(position);
    }
} @EBean
public class CommonActivityTrait {

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

    //private static final int REQUEST_CODE = 20000;

    @RootContext
    Activity activity;

    private boolean isGoingBack = false;

    public void initActivity(Preferences_ preferences) {
        setOrientation(preferences.orientation().get());

        if (activity instanceof MainActivity) {
            return;
        }
        if (activity instanceof ActionBarActivity) {
            ((ActionBarActivity) activity).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        }
    }


    public void setOrientation(String orientation) {
        setOrientation(Integer.valueOf(orientation));
    }

    // http://stackoverflow.com/questions/13171132/disable-the-orientation-change
    public void setOrientation(int orientation) {
        Log.d(TAG, "setOrientation: " + orientation);
        switch (orientation) {
            case Configuration.ORIENTATION_UNDEFINED:
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
                break;
            case Configuration.ORIENTATION_PORTRAIT:
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
                break;
            case Configuration.ORIENTATION_LANDSCAPE:
                activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
                break;
        }
        activity.getResources().getConfiguration().orientation = orientation;
    }

    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case android.R.id.home:
                if (activity.getParent() != null) {
                    isGoingBack = true;
                    activity.finish();
                } else {
                    isGoingBack = true;
                    activity.onBackPressed();
                }
                return true;
        }
        return false;
    }

    public boolean isGoingBack() {
        return isGoingBack;
    }
} @EBean
public class DatabaseHelper {

    //private static final int REQUEST_CODE = 20200;

    @RootContext
    Activity activity;

    @Bean
    Dictionaries dictionaries;

    @OrmLiteDao(helper = DatabaseOpenHelper.class, model = Card.class)
    Dao<Card, Long> cardDao;

    @OrmLiteDao(helper = DatabaseOpenHelper.class, model = Bookmark.class)
    Dao<Bookmark, Long> bookmarkDao;

    // { Card

    public Card createCard(DicItemListView.Data data) throws SQLException {
        dictionaries.reload();
        String display = data.Index.toString().trim();
        String translate = data.Trans.toString().trim();
        Dictionary dictionary = dictionaries.getDictionary(data.getDic());
        return createCard(display, translate, dictionary.getName());
    }

    public Card createCard(String display, String translate, String dictionary) throws SQLException {
        return createCard(display, translate, dictionary, 1);
    }

    public Card createCard(Card card) throws SQLException {
        cardDao.create(card);
        return card;
    }

    public Card createCard(String display, String translate, String dictionary, int box) throws SQLException {
        Card card = getCardByDisplay(display);
        if (card != null) {
            return card;
        }
        card = new Card();
        card.setDisplay(display);
        card.setTranslate(translate);
        card.setDictionary(dictionary != null ? dictionary : "");
        card.setBox(box);
        cardDao.create(card);
        return card;
    }

    public void updateCard(Card card) throws SQLException {
        cardDao.update(card);
    }

    public Card getCardByDisplay(String display) throws SQLException {
        display = display.trim();
        QueryBuilder<Card, Long> queryBuilder = cardDao.queryBuilder();
        queryBuilder.where().like(Card.Column.DISPLAY, new SelectArg(display));
        return queryBuilder.queryForFirst();
    }

    public Cursor findAllCards() throws SQLException {
        return getCursor(cardDao.queryBuilder());
    }

    private Cursor getCursor(QueryBuilder<Card, Long> queryBuilder) throws SQLException {
        CloseableIterator<Card> iterator = cardDao.iterator(queryBuilder.prepare());
        AndroidDatabaseResults results = (AndroidDatabaseResults) iterator.getRawResults();
        return results.getRawCursor();
    }

    public Cursor findCardInBoxAlphabetically(int box) throws SQLException {
        QueryBuilder<Card, Long> queryBuilder = cardDao.queryBuilder();
        queryBuilder.where().eq(Card.Column.BOX, box);
        queryBuilder.orderBy(Card.Column.DISPLAY, true);
        return getCursor(queryBuilder);
    }

    public Cursor findCardInBoxRandomly(int box, int randomSeed) throws SQLException {
        QueryBuilder<Card, Long> queryBuilder = cardDao.queryBuilder();
        queryBuilder.where().eq(Card.Column.BOX, box);
        queryBuilder.orderByRaw(Model.getRandomOrderSql(randomSeed));
        return getCursor(queryBuilder);
    }

    public void deleteCard(Card card) throws SQLException {
        cardDao.delete(card);
    }

    public void deleteAllCards() throws SQLException {
        cardDao.deleteBuilder().delete();
    }

    public Map<Integer, Integer> getCountsForBoxes() throws SQLException {
        Map<Integer, Integer> countsForBoxes = new HashMap<Integer, Integer>();
        GenericRawResults results = cardDao.queryBuilder()
                .groupBy(Card.Column.BOX)
                .selectRaw(String.format("%s, count(%s) AS count", Card.Column.BOX, Card.Column.ID))
                .queryRaw();
        List<String[]> rows = results.getResults();
        for (String[] row : rows) {
            countsForBoxes.put(Integer.valueOf(row[0]), Integer.valueOf(row[1]));
        }
        return countsForBoxes;
    }

    // Card }

    // { Bookmark

    public Bookmark createBookmark(String url, String title, String description) throws SQLException {
        Bookmark bookmark = new Bookmark();
        bookmark.setUrl(url.trim());
        bookmark.setTitle(title.trim());
        bookmark.setDescription(description.trim());
        bookmarkDao.create(bookmark);
        return bookmark;
    }

    public List<Bookmark> findAllBookmarks() throws SQLException {
        QueryBuilder<Bookmark, Long> queryBuilder = bookmarkDao.queryBuilder();
        queryBuilder.orderBy(Bookmark.Column.ID, false);
        return queryBuilder.query();
    }

    public void deleteBookmark(Bookmark bookmark) throws SQLException {
        bookmarkDao.delete(bookmark);
    }

    // Bookmark }
} @EBean
public class SpeechHelper implements TextToSpeech.OnInitListener {

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

    public static final int REQUEST_CODE_TTS = 20300;

    private WeakReference<OnUtteranceListener> onUtteranceListenerRef = new WeakReference<OnUtteranceListener>(null);

    public interface OnUtteranceListener {
        void onUtteranceDone(String utteranceId);

        void onUtteranceError(String utteranceId);
    }

    @Pref
    Preferences_ preferences;

    @RootContext
    Activity activity;

    private TextToSpeech tts = null;

    private String suspendedText = null;

    private boolean autoLanguageDetection = false;

    public void init() {
        Intent intent = new Intent();
        intent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
        activity.startActivityForResult(intent, REQUEST_CODE_TTS);
    }

    public void setAutoLanguageDetection(boolean autoLanguageDetection) {
        this.autoLanguageDetection = autoLanguageDetection;
    }

    @UiThread
    void showProgressMessage() {
        Toast.makeText(activity, R.string.message_in_processing, Toast.LENGTH_SHORT).show();
    }

    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode != TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
            Log.d(TAG, "TTS doesn't have voice data");
            Intent installIntent = new Intent();
            installIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
            activity.startActivity(installIntent);
            return;
        }


        final WeakReference<SpeechHelper> helperRef = new WeakReference<SpeechHelper>(this);
        int listenerResult = -1;

        if (tts == null) {
            tts = new TextToSpeech(activity, this);
            if (Build.VERSION.SDK_INT >= 15) {
                Log.d(TAG, "setUtteranceProgressListener");
                listenerResult = tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
                    @Override
                    public void onStart(String utteranceId) {
                        Log.d(TAG, "onStart: " + utteranceId);
                    }

                    @Override
                    public void onDone(String utteranceId) {
                        Log.d(TAG, "onDone: " + utteranceId);
                        if (helperRef.get() != null) {
                            helperRef.get().onUtteranceDone(utteranceId);
                        }
                    }

                    @Override
                    public void onError(String utteranceId) {
                        Log.d(TAG, "onError: " + utteranceId);
                        if (helperRef.get() != null) {
                            helperRef.get().onUtteranceError(utteranceId);
                        }
                    }
                });
                if (listenerResult != TextToSpeech.SUCCESS) {
                    tts.setOnUtteranceProgressListener(null);
                    Log.d(TAG, "failed to add utterance progress listener");
                }
            }
            if (listenerResult != TextToSpeech.SUCCESS) {
                Log.d(TAG, "setOnUtteranceCompletedListener");
                listenerResult = tts.setOnUtteranceCompletedListener(new TextToSpeech.OnUtteranceCompletedListener() {
                    @Override
                    public void onUtteranceCompleted(String utteranceId) {
                        Log.d(TAG, "onUtteranceCompleted: " + utteranceId);
                        if (helperRef.get() != null) {
                            helperRef.get().onUtteranceDone(utteranceId);
                        }
                    }
                });
                if (listenerResult != TextToSpeech.SUCCESS) {
                    tts.setOnUtteranceCompletedListener(null);
                    Log.d(TAG, "failed to add utterance completed listener");
                }
            }
        }
    }

    @UiThread
    void showTtsIsNotSupported() {
        Toast.makeText(activity, "TTS is not supported", Toast.LENGTH_LONG).show();
    }

    public boolean isProcessing() {
        if (suspendedText != null) {
            return true;
        }
        return tts != null && tts.isSpeaking();
    }

    public void speech(String text) {
        speech(text, null);
    }

    public void speech(String text, HashMap<String, String> params) {
        String defaultLocaleText = preferences.ttsDefaultLocale().get().substring(0, 2).toLowerCase();
        Locale defaultLocale = new Locale(defaultLocaleText);

        Locale locale = EasyLocaleDetector.detectLocale(text);
        if (locale.equals(Locale.ENGLISH)) {
            locale = defaultLocale;
        }
        Log.d(TAG, "Locale selected: " + locale.getDisplayName());
        speech(text, locale, params);
    }

    @Background
    public void speech(String text, Locale speechLocale, HashMap<String, String> params) {
        Log.d(TAG, String.format("speech: (%s) %s", speechLocale.getDisplayName(), text));
        if (tts == null) {
            if (suspendedText == null) {
                suspendedText = text;
                init();
                return;
            }
            showTtsIsNotSupported();
            return;
        }
        tts.setLanguage(speechLocale);
        Log.d(TAG, "TextToSpeech.speak: " + tts.speak(text, TextToSpeech.QUEUE_ADD, buildSpeakParams(params)));
    }

    private HashMap<String, String> buildSpeakParams(HashMap<String, String> additional) {
        HashMap<String, String> params = new HashMap<String, String>();
        params.put(TextToSpeech.Engine.KEY_PARAM_STREAM, String.valueOf(AudioManager.STREAM_MUSIC));
        params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, DateTimeUtils.getInstance().getCurrentDateTimeString());
        if (additional != null) {
            for (String key : additional.keySet()) {
                params.put(key, additional.get(key));
            }
        }
        return params;
    }

    public void finish() {
        Log.d(TAG, "finish");
        if (tts != null) {
            tts.shutdown();
            tts = null;
            suspendedText = null;
        }
    }

    public void setOnUtteranceListener(OnUtteranceListener listener) {
        this.onUtteranceListenerRef = new WeakReference<OnUtteranceListener>(listener);
    }

    @Override
    public void onInit(int status) {
        Log.d(TAG, "onInit: " + status);
        if (status == TextToSpeech.ERROR) {
            suspendedText = null;
            showTtsIsNotSupported();
            return;
        }
        if (suspendedText != null) {
            speech(suspendedText);
            suspendedText = null;
        }
    }

    private void onUtteranceDone(String utteranceId) {
        Log.d(TAG, "onUtteranceDone: " + utteranceId);
        if (onUtteranceListenerRef.get() != null) {
            onUtteranceListenerRef.get().onUtteranceDone(utteranceId);
        }
    }

    private void onUtteranceError(String utteranceId) {
        Log.d(TAG, "onUtteranceError: " + utteranceId);
        if (onUtteranceListenerRef.get() != null) {
            onUtteranceListenerRef.get().onUtteranceError(utteranceId);
        }
    }
} @DatabaseTable(tableName = "bookmark")
public class Bookmark extends Model {

    public static class Column extends Model.Column {
        public static final String URL = "url";
        public static final String TITLE = "title";
        public static final String DESCRIPTION = "description";
    }

    @DatabaseField(canBeNull = false)
    private String url;

    @DatabaseField(canBeNull = false)
    private String title;

    @DatabaseField
    private String description;

    public Bookmark() {
        super();
    }

    public Bookmark(String url, String title) {
        setUrl(url);
        setTitle(title);
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public static void initData(Dao<Bookmark, Long> dao) throws SQLException {
        List<Bookmark> bookmarkData = Arrays.asList(
                new Bookmark("http://m.gutenberg.org/", "Project Gutenberg"),
                new Bookmark("http://en.wikipedia.org/wiki/Main_Page", "Wikipedia"),
                new Bookmark("http://mobile.nytimes.com/international/", "The New York Times"),
                new Bookmark("http://m.bbc.com", "BBC"),
                new Bookmark("http://m.aljazeera.com/index/home", "AJE - Al Jazeera English"),
                new Bookmark("http://www.channelnewsasia.com/mobile", "Channel NewsAsia"),
                new Bookmark("http://www.japantoday.com/smartphone", "Japan Today"));
        for (Bookmark bookmark : bookmarkData) {
            dao.create(bookmark);
        }
    }
} @DatabaseTable(tableName = "card")
public class Card extends Model {

    private static final String TAG_SEPARATOR = "|";
    private static final String TAG_SEPARATOR_PATTERN = "\\|";
    public static final int BOX_MIN = 1;
    public static final int BOX_MAX = 9;

    public static class Column extends Model.Column {
        public static final String DISPLAY = "display";
        public static final String TRANSLATE = "translate";
        public static final String NOTE = "note";
        public static final String BOX = "box";
        public static final String DICTIONARY = "dictionary";
        public static final String TAGS = "tags";
    }

    @DatabaseField(canBeNull = false)
    private String display;

    @DatabaseField
    private String translate;

    @DatabaseField
    private String note;

    @DatabaseField
    private int box = 1;

    @DatabaseField
    private String dictionary;

    @DatabaseField
    private String tags = "";

    public Card() {
        init();
    }

    public Card(String display, String translate, String dictionary) {
        init();
        this.display = display;
        this.translate = translate;
        this.dictionary = dictionary;
        this.box = 1;
    }

    public Card(JsonObject jsonObject) {
        init();
        display = getStringFromJson(jsonObject, Column.DISPLAY);
        translate = getStringFromJson(jsonObject, Column.TRANSLATE);
        dictionary = getStringFromJson(jsonObject, Column.DICTIONARY);
        box = jsonObject.get(Column.BOX).getAsInt();
    }

    public Card(Cursor cursor) throws CursorIndexOutOfBoundsException {
        id = getLongFromCursor(cursor, Column.ID);
        createdAt = getDateFromCursor(cursor, Column.CREATED_AT);
        updatedAt = getDateFromCursor(cursor, Column.UPDATED_AT);
        display = getStringFromCursor(cursor, Column.DISPLAY);
        translate = getStringFromCursor(cursor, Column.TRANSLATE);
        dictionary = getStringFromCursor(cursor, Column.DICTIONARY);
        box = getIntFromCursor(cursor, Column.BOX);
    }

    @Override
    public String toString() {
        return String.format("Card(id=%d, display=%s)", getId(), display);
    }

    public String getShortDisplay() {
        return StringUtils.ellipse(display, 10);
    }

    public String getDisplay() {
        return display;
    }

    public void setDisplay(String display) {
        this.display = display;
    }

    public String getTranslate() {
        return translate;
    }

    public void setTranslate(String translate) {
        this.translate = translate;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }

    public void setBox(int box) {
        if (box < BOX_MIN || BOX_MAX < box) {
            throw new IllegalArgumentException(String.format("box has to be between %d and %d", BOX_MIN, BOX_MAX));
        }
        this.box = box;
    }

    public int getBox() {
        return box;
    }

    public String getDictionary() {
        return dictionary;
    }

    public void setDictionary(String dictionary) {
        this.dictionary = dictionary;
    }

    public List<String> getTags() {
        if (tags == null || tags.isEmpty()) {
            return new ArrayList<String>();
        }
        List<String> tagList = new ArrayList<String>();
        Collections.addAll(tagList, tags.split(TAG_SEPARATOR_PATTERN));
        return tagList;
    }

    public void setTags(List<String> tags) {
        if (tags == null || tags.isEmpty()) {
            this.tags = "";
            return;
        }
        this.tags = TextUtils.join(TAG_SEPARATOR, tags);
    }

    public static void initData(Dao<Card, Long> dao) throws SQLException {
        List<Card> cardData = Arrays.asList(
                new Card("ห้องน้ำ อยู่ที่ไหน ครับ/คะ", "Where is a toilet?", null),
                new Card("トイレはどこですか?", "Where is a toilet?", null),
                new Card("화장실이어디예요", "Where is a toilet?", null),
                new Card("Où sont les toilettes?", "Where is a toilet?", null),
                new Card("Where is a toilet?", "トイレはどこですか?", null));
        for (Card card : cardData) {
            dao.create(card);
        }
    }
} @EBean
public class BrowserActivityState implements JsonPreferencesFieldAdapter.Delegate {

    @Pref
    Preferences_ preferences;

    private JsonPreferencesFieldAdapter adapter;

    @AfterInject
    void afterInject() {
        adapter = new JsonPreferencesFieldAdapter(this, Data.class);
    }

    public String getLastUrl() {
        String lastUrl = getData().lastUrl;
        if (lastUrl == null) {
            lastUrl = "";
        }
        return lastUrl;
    }

    @Background
    public void setLastUrl(String lastUrl) {
        Data data = getData();
        data.lastUrl = lastUrl;
        adapter.saveData(data);
    }

    Data getData() {
        return (Data) adapter.getData();
    }

    @Override
    public String loadJson() {
        return preferences.browserActivityState().get();
    }

    @Override
    public StringPrefEditorField<?> edit() {
        return preferences.edit().browserActivityState();
    }

    static class Data {
        @Expose
        String lastUrl = "";
    }
} @EBean
public class Dictionaries implements JsonPreferencesFieldAdapter.Delegate {

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

    @Pref
    Preferences_ preferences;

    private JsonPreferencesFieldAdapter adapter;

    @AfterInject
    void afterInject() {
        adapter = new JsonPreferencesFieldAdapter(this, Data.class);
        Data data = getData();
        if (data.dictionaries == null) {
            data.dictionaries = new ArrayList<>();
            adapter.saveData(data);
        }
    }

    public void reload() {
        adapter.load();
    }

    public boolean hasDictionary(Dictionary dictionary) {
        for (Dictionary storedDictionary : getData().dictionaries) {
            if (dictionary.equals(storedDictionary)) {
                return true;
            }
        }
        return false;
    }

    public void swap(Dictionary dictionary, int direction) {
        int current = getDictionaryIndex(dictionary);
        Log.d(TAG, String.format("swap: %s %d %d", dictionary, direction, current));
        Data data = getData();
        if (direction == 0) {
            return;
        } else if (direction < 0 && current > 0) {
            data.dictionaries.remove(dictionary);
            data.dictionaries.add(current - 1, dictionary);
        } else if (direction > 0 && current < data.dictionaries.size() - 1) {
            data.dictionaries.remove(dictionary);
            data.dictionaries.add(current + 1, dictionary);
        }
        adapter.saveData(data);
    }

    public int getDictionaryCount() {
        Data data = getData();
        if (data == null || data.dictionaries == null) {
            return 0;
        }
        return data.dictionaries.size();
    }

    public boolean addDictionary(Dictionary dictionary) {
        if (hasDictionary(dictionary)) {
            Log.d(TAG, "Already has the dictionary: " + dictionary);
            return false;
        }
        Data data = getData();
        data.dictionaries.add(dictionary);
        adapter.saveData(data);
        return true;
    }

    public boolean updateDictionary(Dictionary dictionary) {
        if (!hasDictionary(dictionary)) {
            return false;
        }
        Data data = getData();
        Log.d(TAG, "BEFORE: " + toString());
        int i = 0;
        for (Dictionary storedDictionary : data.dictionaries) {
            if (dictionary.equals(storedDictionary)) {
                data.dictionaries.set(i, dictionary);
            }
            i++;
        }
        adapter.saveData(data);
        Log.d(TAG, "AFTER: " + toString());

        return true;
    }

    public Dictionary getDictionaryByPath(String path) {
        for (Dictionary dictionary : getData().dictionaries) {
            if (dictionary.getPath().equals(path)) {
                return dictionary;
            }
        }
        return null;
    }

    public Dictionary getDictionary(int index) {
        Data data = getData();
        if (data.dictionaries.size() - 1 < index) {
            return null;
        }
        return data.dictionaries.get(index);
    }

    public int getDictionaryIndex(Dictionary dictionary) {
        return getData().dictionaries.indexOf(dictionary);
    }

    public void removeDictionary(Dictionary dictionary) {
        removeDictionaryAt(getDictionaryIndex(dictionary));
    }

    public void removeDictionaryAt(int index) {
        Data data = getData();
        data.dictionaries.remove(index);
        adapter.saveData(data);
    }

    Data getData() {
        Data data = (Data) adapter.getData();
        if (data == null) {
            data = new Data();
        }
        return data;
    }

    @Override
    public String toString() {
        return loadJson().toString();
    }

    @Override
    public String loadJson() {
        Log.d(TAG, "loadJson: " + preferences.dictionaries().get());
        return preferences.dictionaries().get();
    }

    @Override
    public StringPrefEditorField<?> edit() {
        return preferences.edit().dictionaries();
    }

    static final class Data {
        @Expose
        List<Dictionary> dictionaries = new ArrayList<>();
    }
} public final class DictionaryService extends Service implements SearchThread.Listener {

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

    // TODO: same as Data.MODE ?
    public static final int DISP_MODE_RESULT = 0;
    public static final int DISP_MODE_NORESULT = 3;
    public static final int DISP_MODE_HEADER = 5;
    public static final int DISP_MODE_END = 10;

    private final Idice dice = DiceFactory.getInstance();

    private final HashMap<String, String> irreg = new HashMap<String, String>();

    private final Binder binder;

    private SearchThread searchThread;

    private WeakReference<Listener> listenerRef = new WeakReference<Listener>(null);

    private boolean isInitialized = false;

    private ArrayList<WeakReference<DicItemListView.Data>> resultData = new ArrayList<WeakReference<DicItemListView.Data>>();

    private Dictionaries dictionaries;

    // TODO: Refactor by EventBus
    public interface Listener {

        void onDictionaryServiceInitialized();

        void onDictionaryServiceUpdateDictionaries();

        void onDictionaryServiceResult(String query, ArrayList<DicItemListView.Data> result);

        void onDictionaryServiceError(String query, Exception e);
    }

    final static class DictionaryServiceBinder extends Binder {

        private WeakReference<DictionaryService> dictionaryServiceRef = new WeakReference<DictionaryService>(null);

        public DictionaryServiceBinder(DictionaryService service) {
            super();
            dictionaryServiceRef = new WeakReference<>(service);
        }

        DictionaryService getService() {
            return dictionaryServiceRef.get();
        }

        void search(String query) {
            Log.d(TAG, "search");
            if (dictionaryServiceRef.get() != null) {
                dictionaryServiceRef.get().searchInThread(query);
            }
        }

        void delete(Dictionary dictionary) {
            Log.d(TAG, "delete");
            if (dictionaryServiceRef.get() != null) {
                dictionaryServiceRef.get().delete(dictionary);
            }
        }

        void swap(Dictionary dictionary, int direction) {
            Log.d(TAG, "swap");
            if (dictionaryServiceRef.get() != null) {
                dictionaryServiceRef.get().swap(dictionary, direction);
            }
        }

        void reload() {
            Log.d(TAG, "reload");
            if (dictionaryServiceRef.get() != null) {
                dictionaryServiceRef.get().reload();
            }
        }

        void setListener(Listener listener) {
            DictionaryService service = getService();
            if (service == null) {
                return;
            }
            service.listenerRef = new WeakReference<>(listener);
            if (service.isInitialized) {
                listener.onDictionaryServiceInitialized();
            }
        }

        void removeListener() {
            if (getService() != null) {
                getService().listenerRef = new WeakReference<>(null);
            }
        }
    }

    public DictionaryService() {
        super();
        binder = new DictionaryServiceBinder(this);
    }

    @Override
    public void onCreate() {
        dictionaries = Dictionaries_.getInstance_(getBaseContext());
        super.onCreate();
        initDice();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    // 英語向けIRREG読込
    private HashMap<String, String> loadIrreg() {
        final String name = "IrregDic.txt";

        try {

            BufferedReader in = new BufferedReader(new InputStreamReader(getAssets().open(name)));

            String str;
            while ((str = in.readLine()) != null) {
                int s = str.indexOf('\t');
                if (s != -1) {
                    String s0 = str.substring(0, s);
                    String s1 = str.substring(s + 1);
                    irreg.put(s0, s1);
                }
            }
            in.close();
            Log.i(TAG, "Open OK:" + name);
            dice.setIrreg(irreg);
        } catch (IOException e) {
            Log.i(TAG, "Open NG:" + name);
        }
        return irreg;
    }

    private synchronized void initDice() {
        dictionaries.reload();
        Log.d(TAG, String.format(
                "dice: %s\ndictionaries: %s",
                dice.toString(),
                dictionaries.toString()));

        loadIrreg();
        for (int i = 0; i < dictionaries.getDictionaryCount(); i++) {
            Dictionary dictionary = dictionaries.getDictionary(i);
            String path = dictionary.getPath();
            IdicInfo dicinfo = dice.open(path);
            if (dicinfo == null) {
                Log.d(TAG, "Already exists: " + path);
                dicinfo = dice.getDicInfo(path);
                if (dicinfo == null) {
                    Log.e(TAG, "Couldn't load dictionary: " + path);
                } else {
                    dicinfo.SetDicName(dictionary.getName());
                    dicinfo.SetEnglish(dictionary.isEnglishIndex());
                    dicinfo.SetNotuse(!dictionary.isEnabled());
                }
            } else {
                Log.d(TAG, "Opened: " + path);
                if (!dicinfo.readIndexBlock(dictionary.createIndexCacheFile(this))) {
                    Log.d(TAG, "Failed to create index");
                    dice.close(dicinfo);
                } else {
                    dicinfo.SetDicName(dictionary.getName());
                    dicinfo.SetEnglish(dictionary.isEnglishIndex());
                    dicinfo.SetNotuse(!dictionary.isEnabled());
                }
            }
        }
        if (!isInitialized) {
            isInitialized = true;
            if (listenerRef.get() != null) {
                listenerRef.get().onDictionaryServiceInitialized();
            }
        }
        Log.d(TAG, String.format(
                "Initialized dice: %s",
                dice.toString()));
        if (listenerRef.get() != null) {
            listenerRef.get().onDictionaryServiceUpdateDictionaries();
        }
    }

    private void searchInThread(String query) {
        Log.d(TAG, "searchInThread: " + query);
        if (!isInitialized) {
            return;
        }
        if (searchThread != null) {
            searchThread.interrupt();
            try {
                searchThread.join();
            } catch (InterruptedException e) {
                // pass
            }
            searchThread = null;
        }
        searchThread = new SearchThread(this, dice, query, 10);
        searchThread.start();
    }

    private void delete(Dictionary dictionary) {
        final IdicInfo dicInfo = dice.getDicInfo(dictionary.getPath());
        if (dicInfo != null) {
            dice.close(dicInfo);
        }
        String cacheFilePath = dictionary.getIndexCacheFilePath(this);
        if (!new File(cacheFilePath).delete()) {
            Log.d(TAG, String.format("Failed to delete dictionary: %s", cacheFilePath));
            return;
        }

        MonodictApp.getEventBus().post(new DictionaryDeletedEvent(dictionary));
    }

    private void swap(Dictionary dictionary, int direction) {
        final IdicInfo dicInfo = dice.getDicInfo(dictionary.getPath());
        dice.swap(dicInfo, direction);
        MonodictApp.getEventBus().post(new DictionarySwappedEvent(dictionary, direction));
    }

    private void reload() {
        initDice();
    }

    // { SearchThread.Interface

    @Override
    public void generateViewForSearch(int mode, int dic, IdicResult pr, ArrayList<DicItemListView.Data> result, int po) {
        switch (mode) {
            case DISP_MODE_RESULT: {
                int count = pr.getCount();
                for (int i = 0; i < count; i++) {
                    DicItemListView.Data data = new DicItemListView.Data(DicItemListView.Data.WORD, dic);

                    String idx = pr.getDisp(i);
                    data.Index = idx;
                    if (idx == null || idx.length() == 0) {
                        data.Index = pr.getIndex(i);
                    }
                    data.Phone = pr.getPhone(i);
                    data.Trans = pr.getTrans(i);
                    data.Sample = pr.getSample(i);

                    if (i == count - 1) {
                        data.LastItem = true;
                    }

                    result.add(data);
                }

                // 結果がまだあるようならmoreボタンを表示
//                    if (dice.hasMoreResult(dic)) {
//                        DicItemListView.Data data = new DicItemListView.Data(DicItemListView.Data.MORE, dic);

//                        data.Index = moreButtonString;

//                            result.add(data);
                break;
            }
            case DISP_MODE_HEADER: {
                DicItemListView.Data data = new DicItemListView.Data(DicItemListView.Data.FOOTER, dic);

                IdicInfo info = dice.getDicInfo(dic);
                Dictionary dictionary = dictionaries.getDictionaryByPath(info.GetFilename());
                if (dictionary != null) {
                    data.Index = dictionary.getName();
                } else {
                    data.Index = info.GetDicName();
                }
                result.add(data);
                break;
            }
            case DISP_MODE_NORESULT: {
                DicItemListView.Data data = new DicItemListView.Data(DicItemListView.Data.NONE, 0);
                data.Index = getResources().getString(R.string.message_no_result);
                result.add(data);
                break;
            }
            case DISP_MODE_END: {
                DicItemListView.Data data = new DicItemListView.Data(DicItemListView.Data.END, 0);
                result.add(data);
                break;
            }
        }
    }

    @Override
    public void onSearchFinished(String query, ArrayList<DicItemListView.Data> result) {
        resultData.clear();
        for (DicItemListView.Data d : result) {
            resultData.add(new WeakReference<>(d));
        }

        if (result.size() < 1) {
            return;
        }
        if (listenerRef.get() != null) {
            listenerRef.get().onDictionaryServiceResult(query, result);
        }
    }

    @Override
    public void onSearchError(String query, Exception e) {
        if (listenerRef.get() != null) {
            listenerRef.get().onDictionaryServiceError(query, e);
        }
    }

    // SearchThread.Interface }

    public static final class DictionaryDeletedEvent {

        private final Dictionary dictionary;

        public DictionaryDeletedEvent(Dictionary dictionary) {
            this.dictionary = dictionary;
        }

        public Dictionary getDictionary() {
            return dictionary;
        }
    }

    public static final class DictionarySwappedEvent {

        private final Dictionary dictionary;

        private final int direction;

        public DictionarySwappedEvent(Dictionary dictionary, int direction) {
            this.dictionary = dictionary;
            this.direction = direction;
        }

        public Dictionary getDictionary() {
            return dictionary;
        }

        public int getDirection() {
            return direction;
        }
    }
} public final class DictionaryServiceConnection implements ServiceConnection {

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

    private DictionaryService.DictionaryServiceBinder binder;

    private WeakReference<DictionaryService.Listener> listenerRef = new WeakReference<>(null);

    private boolean isConnected = false;

    public DictionaryServiceConnection() {
    }

    public void setListener(DictionaryService.Listener listener) {
        listenerRef = new WeakReference<>(listener);
    }

    public void search(String query) {
        if (!isConnected) {
            return;
        }
        binder.search(query);
    }

    public void deleteDictionary(Dictionary dictionary) {
        if (!isConnected) {
            return;
        }
        binder.delete(dictionary);
    }

    public void swap(Dictionary dictionary, int direction) {
        if (!isConnected) {
            return;
        }
        binder.swap(dictionary, direction);
    }

    public void reload() {
        if (!isConnected) {
            return;
        }
        binder.reload();
    }


    public boolean isConnected() {
        return isConnected;
    }

    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        Log.d(TAG, "onServiceConnected");
        binder = ((DictionaryService.DictionaryServiceBinder) iBinder);
        if (listenerRef.get() != null) {
            binder.setListener(listenerRef.get());
        }
        isConnected = true;
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        binder.removeListener();
        binder = null;
    }
} public class DicItemListView extends ListView {

    public static class Data {
        public static final int WORD = 0;
        public static final int MORE = 1;
        public static final int NONE = 2;
        public static final int NORESULT = 3;
        public static final int FOOTER = 4;
        public static final int END = 10;

        private int mDic;
        private int mMode;
        public CharSequence Index;
        public CharSequence Phone;
        public CharSequence Trans;
        public CharSequence Sample;

        public boolean LastItem = false;
        public Typeface IndexFont;
        public Typeface PhoneFont;
        public Typeface TransFont;
        public Typeface SampleFont;
        public int IndexSize;
        public int PhoneSize;
        public int TransSize;
        public int SampleSize;

        public Data(int mode, int dic) {
            mDic = dic;
            mMode = mode;
        }

        @Override
        public String toString() {
            String index = Index != null ? Index.toString() : "";
            String phone = Phone != null ? Phone.toString() : "";
            String trans = Trans != null ? Trans.toString() : "";
            String sample = Sample != null ? Sample.toString() : "";
            return String.format("index: %s\nphone: %s\ntrans: %s\nsample: %s", index, phone, trans, sample);
        }

        public String toSummaryString() {
            String all = Index.toString();
            if (Trans != null) {
                all += "\n";
                all += Trans;
            }

            if (Sample != null) {
                all += "\n";
                all += Sample;
            }

            all += "\n";
            return all;
        }

        public List<Pair<String, String>> getAdditionalLinkInfo() {
            List<Pair<String, String>> linkPairs = new ArrayList<Pair<String, String>>();

            // <→リンク> 英辞郎形式
            {
                Pattern p = Pattern.compile("<(→(.+?))>");
                Matcher m = p.matcher(Trans);

                while (m.find()) {
                    linkPairs.add(new Pair<>(m.group(1), m.group(2)));
                }
            }
            // "→　" 和英辞郎形式
            {
                Pattern p = Pattern.compile("(→　(.+))");
                Matcher m = p.matcher(Trans);

                while (m.find()) {
                    linkPairs.add(new Pair<>(m.group(1), m.group(2)));
                }
            }

            // "＝リンク●" 略辞郎形式
            {
                Pattern p = Pattern.compile("(＝(.+))●");
                Matcher m = p.matcher(Trans);

                while (m.find()) {
                    linkPairs.add(new Pair<>(m.group(1), m.group(2)));
                }
            }
            return linkPairs;
        }

        public int getDic() {
            return mDic;
        }

        public int getMode() {
            return mMode;
        }
    }

    public interface Callback {
        void onDicItemActionAddToFlashcard(int position);

        void onDicItemClickSpeechButton(int position);

        void onDicItemActionShare(int position);

        void onDicItemActionSearch(int position);

        void onDicItemActionCopyAll(int position);

        void onDicItemActionModeSearch(String selectedText);

        void onDicItemActionModeSpeech(String selectedText);
    }

    Callback callback;

    private void init(Context context) {
        setSmoothScrollbarEnabled(true);
        setScrollingCacheEnabled(true);
        setFocusable(true);
        setFocusableInTouchMode(true);
        setFastScrollEnabled(true);
        setCacheColorHint(Color.WHITE);
    }

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

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

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

    public void setCallback(Callback cb) {
        callback = cb;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        requestFocus();
        return super.onTouchEvent(ev);
    }

    static class ActionModeCallback implements ActionMode.Callback {

        private WeakReference<TextView> textViewRef;

        ActionModeCallback(Context context, TextView textView) {
            this.textViewRef = new WeakReference<>(textView);
        }

        @Override
        public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
            for (int i = 0; i < menu.size(); i++) {
                MenuItem item = menu.getItem(i);
                item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
            }

            MenuInflater inflater = actionMode.getMenuInflater();
            inflater.inflate(R.menu.main_action_mode, menu);
            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
            if (textViewRef.get() == null) {
                return false;
            }
            TextView textView = textViewRef.get();
            ViewParent parentView = ViewUtils.findParentView(textView, DicItemListView.class);
            int selectionStart = textView.getSelectionStart();
            int selectionEnd = textView.getSelectionEnd();
            int min = Math.max(0, Math.min(selectionStart, selectionEnd));
            int max = Math.max(0, Math.max(selectionStart, selectionEnd));
            String selectedText = textView.getText().subSequence(min, max).toString();
            Callback callback = ((DicItemListView) parentView).callback;

            if (menuItem.getItemId() == R.id.action_search) {
                if (callback != null) {
                    callback.onDicItemActionModeSearch(selectedText);
                }
                actionMode.finish();
                return true;
            }
            if (menuItem.getItemId() == R.id.action_speech) {
                if (callback != null) {
                    callback.onDicItemActionModeSpeech(selectedText);
                }
                actionMode.finish();
                return true;
            }
            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode actionMode) {
        }
    }

    public static class ResultAdapter extends ArrayAdapter<Data> {

        static class ViewHolder {
            TextView Index;
            TextView Phone;
            TextView Trans;
            ImageButton addToFlashcardButton;
            ImageButton speechButton;
            ImageButton actionButton;
            TextView Sample;
            View hr;
            View end;
        }

        public static interface DictionaryDataSource {
            Dictionary getDictionaryForDicItemListAdapter(int index);
        }

        private String highlightKeyword = null;

        private DictionaryDataSource dictionaryDataSource;

        public ResultAdapter(Context context, int resource, int textViewResourceId, ArrayList<Data> objects) {
            super(context, resource, textViewResourceId, objects);
        }

        public void setDictionaryDataSource(DictionaryDataSource dataSource) {
            this.dictionaryDataSource = dataSource;
        }

        public void setHighlightKeyword(String highlightKeyword) {
            this.highlightKeyword = highlightKeyword;
        }

        private Spanned getRichTranslateText(Data data) {
            // http://www.eijiro.jp/e/spec.htm
            String richText = data.Trans.toString();
            richText = richText
                    .trim()
                    .replace("<", "＜")
                    .replace(">", "＞")
                    .replaceAll("＜(.?)→(.*)＞", "＜→ $2 ＞"); // Search "jasper" to check this
            try {
                richText = richText.replaceAll("(?i)" + highlightKeyword, "<b>" + highlightKeyword + "</b>");
            } catch (PatternSyntaxException e) {
                // TODO: Multi bytes
            }
            richText = richText.replace("\n", "<br />").trim();

            return Html.fromHtml(richText);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            final View view;
            final ViewHolder holder;
            if (convertView != null && (convertView instanceof LinearLayout)) {
                view = convertView;
                holder = (ViewHolder) view.getTag();
            } else {
                view = inflate(getContext(), R.layout.list_item_dic, null);
                final Context context = getContext();

                holder = new ViewHolder();

                holder.Index = (TextView) view.findViewById(R.id.ListIndex);
                holder.Index.setCustomSelectionActionModeCallback(new ActionModeCallback(context, holder.Index));

                holder.Phone = (TextView) view.findViewById(R.id.ListPhone);
                holder.Phone.setCustomSelectionActionModeCallback(new ActionModeCallback(context, holder.Phone));

                holder.Trans = (TextView) view.findViewById(R.id.ListTrans);
                holder.Trans.setCustomSelectionActionModeCallback(new ActionModeCallback(context, holder.Trans));

                holder.Sample = (TextView) view.findViewById(R.id.ListSample);
                holder.Sample.setCustomSelectionActionModeCallback(new ActionModeCallback(context, holder.Sample));

                holder.addToFlashcardButton = (ImageButton) view.findViewById(R.id.add_to_flashcard_button);
                holder.addToFlashcardButton.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        View rowView = (View) ViewUtils.findParentView(view, R.id.root);
                        DicItemListView listView = (DicItemListView) rowView.getParent();
                        if (listView.callback != null) {
                            listView.callback.onDicItemActionAddToFlashcard(listView.getPositionForView(rowView));
                        }
                    }
                });
                holder.speechButton = (ImageButton) view.findViewById(R.id.speech_button);
                holder.speechButton.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        View rowView = (View) ViewUtils.findParentView(view, R.id.root);
                        DicItemListView listView = (DicItemListView) rowView.getParent();
                        if (listView.callback != null) {
                            listView.callback.onDicItemClickSpeechButton(listView.getPositionForView(rowView));
                        }
                    }
                });
                holder.actionButton = (ImageButton) view.findViewById(R.id.action_button);
                holder.actionButton.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        View rowView = (View) ViewUtils.findParentView(view, R.id.root);
                        DicItemListView listView = (DicItemListView) rowView.getParent();
                        PopupMenu popupMenu = new PopupMenu(context, holder.actionButton);
                        popupMenu.inflate(R.menu.popup_dic);
                        final int position  = listView.getPositionForView(rowView);
                        final Callback listViewCallback = listView.callback;
                        popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                            @Override
                            public boolean onMenuItemClick(MenuItem item) {
                                switch (item.getItemId()) {
                                    case R.id.action_copy_all:
                                        listViewCallback.onDicItemActionCopyAll(position);
                                        return true;
                                    case R.id.action_search:
                                        listViewCallback.onDicItemActionSearch(position);
                                        return true;
                                    case R.id.action_share:
                                        listViewCallback.onDicItemActionShare(position);
                                        return true;
                                }
                                return false;
                            }
                        });
                        popupMenu.show();
                    }
                });
                holder.hr = view.findViewById(R.id.hr);
                holder.end = view.findViewById(R.id.end);

                holder.Index.setTextColor(Color.BLACK);
                holder.Phone.setTextColor(Color.BLACK);
                holder.Trans.setTextColor(Color.BLACK);
                holder.Sample.setTextColor(Color.BLACK);

                view.setTag(holder);
            }
            Data d = getItem(position);
            DictionaryFont indexFont = DictionaryFont.getDefaultIndexFont();
            DictionaryFont phoneFont = DictionaryFont.getDefaultPhoneFont();
            DictionaryFont transFont = DictionaryFont.getDefaultTransFont();
            DictionaryFont sampleFont = DictionaryFont.getDefaultSampleFont();
            if (dictionaryDataSource != null) {
                Dictionary dictionary = dictionaryDataSource.getDictionaryForDicItemListAdapter(d.mDic);
                if (dictionary != null) {
                    indexFont = dictionary.getIndexFont();
                    phoneFont = dictionary.getPhoneFont();
                    transFont = dictionary.getTransFont();
                    sampleFont = dictionary.getSampleFont();
                }
            }

            holder.addToFlashcardButton.setVisibility(View.GONE);
            holder.speechButton.setVisibility(View.GONE);
            holder.actionButton.setVisibility(View.GONE);
            holder.hr.setVisibility(View.VISIBLE);
            holder.end.setVisibility(View.GONE);
            View header = view.findViewById(R.id.header);
            header.setVisibility(View.GONE);
            View content = view.findViewById(R.id.content);
            content.setVisibility(View.VISIBLE);

            setItem(holder.Index, d.Index, d.IndexFont, Typeface.NORMAL, 18);

            Context context = getContext();

            switch (d.getMode()) {
                case Data.WORD:
                    setItem(holder.Index,
                            d.Index,
                            indexFont.getTypeface(context),
                            indexFont.getStyle(),
                            indexFont.getSize());
                    setItem(holder.Phone,
                            d.Phone,
                            phoneFont.getTypeface(context),
                            phoneFont.getStyle(),
                            phoneFont.getSize());
                    setItem(holder.Trans,
                            getRichTranslateText(d),
                            transFont.getTypeface(context),
                            transFont.getStyle(),
                            transFont.getSize());
                    String sample = "";
                    if (d.Sample != null) {
                        sample = "“" + d.Sample + "”";
                    }
                    setItem(holder.Sample,
                            sample,
                            sampleFont.getTypeface(context),
                            sampleFont.getStyle(),
                            sampleFont.getSize());

                    holder.addToFlashcardButton.setVisibility(View.VISIBLE);
                    holder.speechButton.setVisibility(View.VISIBLE);
                    holder.actionButton.setVisibility(View.VISIBLE);
                    if (d.LastItem) {
                        holder.hr.setVisibility(View.GONE);
                    }
                    break;
                case Data.MORE:
                    holder.Index.setVisibility(View.GONE);
                    holder.Phone.setVisibility(View.GONE);
                    holder.Trans.setVisibility(View.GONE);
                    holder.Sample.setVisibility(View.GONE);
                    break;
                case Data.NORESULT:
                case Data.NONE:
                    setItem(holder.Index, d.Index, d.IndexFont, Typeface.NORMAL, 0);
                    holder.Phone.setVisibility(View.GONE);
                    holder.Trans.setVisibility(View.GONE);
                    holder.Sample.setVisibility(View.GONE);
                    break;
                case Data.FOOTER:
                    header.setVisibility(View.VISIBLE);
                    ((TextView) header.findViewById(R.id.header_text)).setText(d.Index);
                    content.setVisibility(View.GONE);

                    setItem(holder.Index, d.Index, d.IndexFont, Typeface.NORMAL, 14);
                    holder.Index.setVisibility(View.VISIBLE);
                    holder.Phone.setVisibility(View.GONE);
                    holder.Trans.setVisibility(View.GONE);
                    holder.Sample.setVisibility(View.GONE);
                    holder.hr.setVisibility(View.GONE);
                    break;
                case Data.END:
                    content.setVisibility(View.GONE);
                    holder.end.setVisibility(View.VISIBLE);
                default:
                    holder.Index.setVisibility(View.GONE);
                    holder.Phone.setVisibility(View.GONE);
                    holder.Trans.setVisibility(View.GONE);
                    holder.Sample.setVisibility(View.GONE);
            }
            return view;
        }

        private void setItem(TextView tv, CharSequence str, Typeface tf, int style, int size) {
            if (str == null || str.length() == 0) {
                tv.setVisibility(View.GONE);
            } else {
                tv.setVisibility(View.VISIBLE);
                tv.setText(str);
                if (tf != null) {
                    tv.setTypeface(tf, style);
                }
                if (size > 10) {
                    tv.setTextSize(size);
                }
            }
        }
    }
}
 @EFragment(R.layout.fragment_translate_panel)
public class TranslatePanelFragment extends Fragment {

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

    private static final int TRANSLATE_PANEL_DURATION = 150;

    @ViewById
    TextView displayText;

    @ViewById
    TextView translateText;

    @ViewById
    TextView dictionaryNameText;

    private DicItemListView.Data data;

    private Listener listener;

    public interface Listener {
        void onClickTranslatePanelAddToFlashcardButton(DicItemListView.Data data);

        void onClickTranslatePanelSpeechButton(DicItemListView.Data data);
    }

    public void show() {
        Log.d(TAG, "show");
        View view = getView();
        if (view == null) {
            return;
        }
        view.setVisibility(View.VISIBLE);
        Animation animation = AnimationUtils.makeInChildBottomAnimation(getActivity());
        animation.setDuration(TRANSLATE_PANEL_DURATION);
        view.startAnimation(animation);
    }

    public void hide() {
        Log.d(TAG, "hide");
        View view = getView();
        if (view == null) {
            return;
        }
        view.setVisibility(View.GONE);
    }

    public void setListener(Listener listener) {
        this.listener = listener;
    }

    public void setData(DicItemListView.Data data) {
        this.data = data;
        displayText.setText(data.Index.toString());
        translateText.setScrollY(0);
        translateText.setText(data.Trans.toString());
    }

    public void setDictionaryName(String dictionaryName) {
        dictionaryNameText.setText(dictionaryName);
    }

    @Click(R.id.more_button)
    void onClickTranslatePanelMoreButton() {
        ActivityHelper_.getInstance_(getActivity()).searchOnMainActivity(data.Index.toString());
    }

    @Click(R.id.speech_button)
    void onClickTranslatePanelSpeechButton() {
        listener.onClickTranslatePanelSpeechButton(data);
    }

    @Click(R.id.close_button)
    void onClickTranslatePanelCloseButton() {
        hide();
    }

    @Click(R.id.add_to_flashcard_button)
    void onClickTranslatePanelAddToFlashcardButton() {
        listener.onClickTranslatePanelAddToFlashcardButton(data);
    }

    // Possible Memory Leaks and missing onDestroyView cleanup
    // https://github.com/excilys/androidannotations/issues/933
    @Override
    public void onDestroyView() {
        super.onDestroyView();

        displayText = null;
        translateText = null;
        dictionaryNameText = null;
        listener = null;
    }
} public class ViewUtils {

    private static final int MAX_DEPTH = 100;

    public static ViewParent findParentView(View rootView, Class findClass) {
        View currentView = rootView;
        for (int i = 0; i < MAX_DEPTH; i++) {
            ViewParent parentView = currentView.getParent();
            if (parentView == null) {
                return null;
            }
            if (findClass.isInstance(parentView)) {
                return parentView;
            }
            currentView = (View) parentView;
        }

        return null;
    }

    public static ViewParent findParentView(View rootView, int viewId) {
        View currentView = rootView;
        for (int i = 0; i < MAX_DEPTH; i++) {
            ViewParent parentView = currentView.getParent();
            if (parentView == null) {
                return null;
            }
            if (((View) parentView).getId() == viewId) {
                return parentView;
            }
            currentView = (View) parentView;
        }

        return null;
    }

    public static void addMarginBottom(View view, int marginBottom) {
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        params.setMargins(
                params.leftMargin,
                params.topMargin,
                params.rightMargin,
                params.bottomMargin + marginBottom);
        view.requestLayout();
    }
}
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.util.Log;
import android.view.ActionMode;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.webkit.ValueCallback;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.github.wakhub.monodict.MonodictApp;
import com.github.wakhub.monodict.R;
import com.github.wakhub.monodict.activity.bean.ActivityHelper;
import com.github.wakhub.monodict.activity.bean.CommonActivityTrait;
import com.github.wakhub.monodict.activity.bean.DatabaseHelper;
import com.github.wakhub.monodict.activity.bean.SpeechHelper;
import com.github.wakhub.monodict.db.Bookmark;
import com.github.wakhub.monodict.db.Card;
import com.github.wakhub.monodict.preferences.BrowserActivityState;
import com.github.wakhub.monodict.preferences.Dictionaries;
import com.github.wakhub.monodict.preferences.Preferences_;
import com.github.wakhub.monodict.search.DictionaryService;
import com.github.wakhub.monodict.search.DictionaryServiceConnection;
import com.github.wakhub.monodict.ui.DicItemListView;
import com.github.wakhub.monodict.ui.TranslatePanelFragment;
import com.github.wakhub.monodict.utils.ViewUtils;
import com.melnykov.fab.FloatingActionButton;
import com.nispok.snackbar.Snackbar;
import com.nispok.snackbar.SnackbarManager;
import com.nispok.snackbar.enums.SnackbarType;
import com.nispok.snackbar.listeners.ActionClickListener;
import com.nispok.snackbar.listeners.EventListener;
import org.androidannotations.annotations.AfterViews;
import org.androidannotations.annotations.Background;
import org.androidannotations.annotations.Bean;
import org.androidannotations.annotations.Click;
import org.androidannotations.annotations.EActivity;
import org.androidannotations.annotations.Extra;
import org.androidannotations.annotations.FragmentById;
import org.androidannotations.annotations.OnActivityResult;
import org.androidannotations.annotations.OptionsItem;
import org.androidannotations.annotations.OptionsMenu;
import org.androidannotations.annotations.UiThread;
import org.androidannotations.annotations.ViewById;
import org.androidannotations.annotations.sharedpreferences.Pref;
import java.lang.ref.WeakReference;
import java.sql.SQLException;
import java.util.ArrayList;
/**
 * Copyright (C) 2014 wak
 *
 * 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.github.wakhub.monodict.activity;





@EActivity(R.layout.activity_browser)
@OptionsMenu({R.menu.browser})
public class BrowserActivity extends ActionBarActivity implements
        DictionaryService.Listener,
        TranslatePanelFragment.Listener,
        TextView.OnEditorActionListener,
        BrowserActivityWebView.ActionModeListener,
        BrowserActivityJavaScriptInterface.Listener {

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

    private static final int REQUEST_CODE_BOOKMARKS = 10020;
    private static final int REQUEST_CODE_OPEN_LOCAL_FILE = 10021;

    private static final String ENCODING = "utf-8";

    private static final String JAVASCRIPT_CALLBACK_SEARCH = "search";
    private static final String JAVASCRIPT_CALLBACK_SPEECH = "speech";

    private static final String[] PROTOCOLS = new String[]{"http://", "https://", "ftp://", "sftp://", "file://"};

    @Extra
    String extraUrlOrKeywords = "";

    @ViewById
    EditText urlText;

    @ViewById
    BrowserActivityWebView webView;

    @ViewById
    FloatingActionButton backButton;

    @ViewById
    FloatingActionButton nextButton;

    @ViewById
    FloatingActionButton refreshButton;

    @ViewById
    ProgressBar progressBar;

    @FragmentById
    TranslatePanelFragment translatePanelFragment;

    @Pref
    Preferences_ preferences;

    @Bean
    CommonActivityTrait commonActivityTrait;

    @Bean
    ActivityHelper activityHelper;

    @Bean
    DatabaseHelper databaseHelper;

    @Bean
    BrowserActivityState state;

    @Bean
    SpeechHelper speechHelper;

    @Bean
    Dictionaries dictionaries;