net.robotmedia.acv.ui.widget.OcrLayout.java Source code

Java tutorial

Introduction

Here is the source code for net.robotmedia.acv.ui.widget.OcrLayout.java

Source

/*******************************************************************************
 * Copyright 2013-2016 Christopher Brochtrup
 *
 * 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 net.robotmedia.acv.ui.widget;

import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.support.annotation.StringRes;
import android.support.v4.app.ActivityCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.Display;
import android.view.GestureDetector.OnGestureListener;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.cb4960.dic.Dic;
import com.cb4960.dic.DicEdict;
import com.cb4960.dic.DicEpwing;
import com.cb4960.dic.DicEpwingRaw;
import com.cb4960.dic.DicKanji;
import com.cb4960.dic.DicNames;
import com.cb4960.dic.Entry;
import com.cb4960.dic.Example;
import com.cb4960.dic.FineTune;
import com.cb4960.dic.Frequency;
import com.cb4960.dic.UtilsCommon;
import com.cb4960.dic.UtilsFormatting;
import com.cb4960.dic.UtilsLang;
import com.cb4960.dic.WordSet;
import com.cb4960.ocrmr.R;
import com.googlecode.leptonica.android.Binarize;
import com.googlecode.leptonica.android.Clip;
import com.googlecode.leptonica.android.Convert;
import com.googlecode.leptonica.android.Convolve;
import com.googlecode.leptonica.android.Enhance;
import com.googlecode.leptonica.android.Pix;
import com.googlecode.leptonica.android.ReadFile;
import com.googlecode.leptonica.android.Scale;
import com.googlecode.leptonica.android.Seedfill;
import com.googlecode.tesseract.android.TessBaseAPI;
import com.ichi2.anki.api.AddContentApi;

import net.robotmedia.acv.Constants;
import net.robotmedia.acv.logic.PreferencesController;
import net.robotmedia.acv.ui.ComicViewerActivity;
import net.robotmedia.acv.utils.AnkiUtils;
import net.robotmedia.acv.utils.BoundingTextRect;
import net.robotmedia.acv.utils.FileUtils;
import net.robotmedia.acv.utils.Furigana;
import net.robotmedia.acv.utils.IntentUtils;
import net.robotmedia.acv.utils.LeptUtils;
import net.robotmedia.acv.utils.MathUtils;
import net.robotmedia.acv.utils.ShellUtils;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Stack;
import java.util.Timer;
import java.util.TimerTask;

/** Container for all OCR related views. */
public class OcrLayout extends RelativeLayout implements OnGestureListener {
    /** Direction to nudge the capture box. */
    public enum NudgeDirection {
        UP, DOWN, LEFT, RIGHT
    }

    /** Part of capture box that is being dragged. */
    private enum DragRegion {
        NONE, TOP_LEFT, TOP, TOP_RIGHT, LEFT, MIDDLE, RIGHT, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT
    }

    /** Screen quadrant. */
    private enum Quadrant {
        TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT,
    }

    /** Location of the dictionary view. */
    private enum DicViewLocation {
        TOP_LEFT, TOP, TOP_RIGHT, LEFT, RIGHT, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT
    }

    /** State that described current capture box manipulation. */
    private enum CaptureState {
        SET_TOP_LEFT, SET_BOTTOM_RIGHT, DRAG
    }

    /** Amount to scale the capture before passing it to the OCR engine. */
    final public static float SCALE_FACTOR = 3.5f;

    /** Tag to use in log. */
    final private String LOG_TAG = "OcrLayout";

    // Orientation of the text that is being captured
    final private int TEXT_ORIENTATION_VERTICAL = 0;
    final private int TEXT_ORIENTATION_HORIZONTAL = 1;
    final private int TEXT_ORIENTATION_AUTO = 2;

    // Corner of capture box that nudge buttons will adjust
    final private int NUDGE_CORNER_TOP_LEFT = 0;
    final private int NUDGE_CORNER_BOTTOM_RIGHT = 1;

    public static final int ANKI_RW_PERM_REQ_CODE = 4261;

    /** View that contains the comic image. */
    private ComicView comicView = null;

    /** View that hosts anything that needs to be drawn, such as the capture box. */
    private OcrView ocrView = null;

    /** Capture box. */
    private Rect captureBox = null;

    /** Represents the top-left point of the foreground clip that is generated during pre-processing. */
    private Point clipOffset = new Point(0, 0);

    /** Previous capture box. Set in the capture timer.
     *  Compared against captureBox to determine if screen capture and OCR should take place. */
    private Rect lastCaptureBox = new Rect(0, 0, 1, 1);

    /** State of the capture. */
    private CaptureState captureState = CaptureState.SET_TOP_LEFT;

    /** Which region is being dragged by the user? */
    private DragRegion dragRegion = DragRegion.NONE;

    /** Last capture of the _entire_ comicView screen. */
    private Bitmap lastScreenshot = null;

    /** Canvas that will draw the ComicView onto lastScreenshot. */
    private Canvas captureCanvas = null;

    /** Last captured screen area used as input into the OCR engine. */
    private Pix lastCapture = null;

    /** Usable screen width. */
    private int screenWidth = 0;

    /** Usable screen height (excludes the navigation bar if < Android 4.4). */
    private int screenHeight = 0;

    /** Tesseract OCR library. */
    private static TessBaseAPI tess = null;

    /** OCR'd text from the latest capture. */
    private String lastOcrText = "";

    /** Used for next/previous navigation. Stores the indices of words in lastOcrText. */
    private Stack<Integer> lookupWordIdxStack = new Stack<Integer>();

    /** Bounding boxes around the OCR'd text */
    private List<Rect> boundingBoxes;

    /** Used to schedule screen captures and OCR. */
    private Timer captureTimer;

    /** The task that is run by captureTimer. */
    private UpdateCaptureTask updateCaptureTask;

    /** The last time that updateCaptureTask checked to see if it should capture and OCR. */
    private Calendar lastCaptureTime = null;

    /** The last time that the capture box was adjusted */
    private Calendar lastAdjustment = null;

    /** EDICT dictionary */
    private static DicEdict dicEdict = new DicEdict();

    /** Names dictionary */
    private static DicNames dicNames = new DicNames();

    /** Kanji dictionary */
    private static DicKanji dicKanji = new DicKanji();

    /** List of dictionary entries */
    private List<Entry> lastEntryList = null;

    /** WebView that will contain the dictionary lookup. */
    private WebView dicView = null;

    /** Set to force a capture and OCR by the capture timer task. */
    private boolean forceUpdate = false;

    /** Corner of capture box that the nudge buttons will adjust. */
    private int nudgeCorner = this.NUDGE_CORNER_BOTTOM_RIGHT;

    /** True = GUI is currently hidden (except for the Show/Hide button). */
    private boolean hideGui = false;

    /** Used to get/store settings */
    private PreferencesController preferencesController = null;

    /** Context passed in by the constructor. */
    private Context context = null;

    /** Orientation of the text. */
    private int textOrientation = this.TEXT_ORIENTATION_VERTICAL;

    /** Progress dialog used when copying database assets.*/
    private ProgressDialog assetCopyProgressDialog = null;

    /** Tesseract database file */
    private File tesseractDbFile = null;

    /** Directory that contains the Tesseract database */
    private File tesseractDbDir = null;

    /** Edict database file */
    private File edictDbFile = null;

    /** Names database file */
    private File namesDbFile = null;

    /** Kanji database file */
    private File kanjiDbFile = null;

    /** Kanji definition format file */
    private File kanjiDefFormatFile = null;

    /** De-inflection rules file */
    private File deinflectionDbFile = null;

    /** Substitutions file */
    private File substitutionsDbFile = null;

    /** From->To substitutions found in the substitutions file */
    private List<Pair<String, String>> substitutionsList = null;

    /** Frequency database file */
    private File freqDbFile = null;

    /** Frequency database */
    private static Frequency freqDb = new Frequency();

    /** Word set for known words */
    private WordSet wordSetKnown = null;

    /** Word set to todo words */
    private WordSet wordSetTodo = null;

    /** Reference to the comic viewer activity */
    private ComicViewerActivity comicViewerActivity = null;

    /** Has the OCR view been started? */
    private boolean ocrStarted = false;

    /** Original width of nudge buttons */
    private int origNudgeButtonsWidth = 40;

    /** Original height of nudge buttons */
    private int origNudgeButtonsHeight = 40;

    /** True = Trigger capture in progress */
    private boolean isTriggerCapture = false;

    // GUI references
    private Button btnSwapNudgeCorner = null;
    private Button btnUp = null;
    private Button btnDown = null;
    private Button btnLeft = null;
    private Button btnRight = null;
    private Button btnTextOrientation = null;
    private Button btnSend = null;
    private Button btnLookupNext = null;
    private Button btnLookupPrev = null;
    private Button btnMenu = null;
    private Button btnExit = null;
    private TextView textViewStartMsg = null;

    // Settings
    private int ocrSettingsDictBackgroundColor = Constants.DEFAULT_OCR_SETTINGS_DICT_BACKGROUND_COLOR;
    private int ocrSettingsDictExpressionColor = Constants.DEFAULT_OCR_SETTINGS_DICT_EXPRESSION_COLOR;
    private int ocrSettingsDictReadingColor = Constants.DEFAULT_OCR_SETTINGS_DICT_READING_COLOR;
    private int ocrSettingsDictDefinitionColor = Constants.DEFAULT_OCR_SETTINGS_DICT_DEFINITION_COLOR;
    private int ocrSettingsDictConjugationColor = Constants.DEFAULT_OCR_SETTINGS_DICT_CONJUGATION_COLOR;
    private int ocrSettingsDictSubDefColor = Constants.DEFAULT_OCR_SETTINGS_DICT_SUB_DEF_COLOR;
    private int ocrSettingsDictExamplePrependColor = Constants.DEFAULT_OCR_SETTINGS_DICT_EXAMPLE_PREPEND_COLOR;
    private int ocrSettingsDictExampleJapColor = Constants.DEFAULT_OCR_SETTINGS_DICT_EXAMPLE_JAP_COLOR;
    private int ocrSettingsDictExampleEngColor = Constants.DEFAULT_OCR_SETTINGS_DICT_EXAMPLE_ENG_COLOR;
    private int ocrSettingsDictNameColor = Constants.DEFAULT_OCR_SETTINGS_DICT_NAME_COLOR;
    private int ocrSettingsDictSeparatorColor = Constants.DEFAULT_OCR_SETTINGS_DICT_SEPARATOR_COLOR;
    private int ocrSettingsDictOcrTextColor = Constants.DEFAULT_OCR_SETTINGS_DICT_OCR_TEXT_COLOR;
    private int ocrSettingsCaptureBoxColor = Constants.DEFAULT_OCR_SETTINGS_CAPTURE_BOX_COLOR;
    private int ocrSettingsBoundingBoxColor = Constants.DEFAULT_OCR_SETTINGS_BOUNDING_BOX_COLOR;
    private int ocrSettingsFreqVeryCommonColor = Constants.DEFAULT_OCR_SETTINGS_FREQ_VERY_COMMON_COLOR;
    private int ocrSettingsFreqCommonColor = Constants.DEFAULT_OCR_SETTINGS_FREQ_COMMON_COLOR;
    private int ocrSettingsFreqUncommonColor = Constants.DEFAULT_OCR_SETTINGS_FREQ_UNCOMMON_COLOR;
    private int ocrSettingsFreqRareColor = Constants.DEFAULT_OCR_SETTINGS_FREQ_RARE_COLOR;
    private int ocrSettingsWordHighlightColor = Constants.DEFAULT_OCR_SETTINGS_WORD_HIGHLIGHT_COLOR;
    private int ocrSettingsKnownWordColor = Constants.DEFAULT_OCR_SETTINGS_KNOWN_WORD_COLOR;

    private boolean ocrSettingsShowBoundingBoxes = Constants.DEFAULT_OCR_SETTINGS_SHOW_BOUNDING_BOXES;
    private boolean ocrSettingsSimplifiedLayoutPortrait = Constants.DEFAULT_OCR_SETTINGS_SIMPLIFIED_LAYOUT_PORTRAIT;
    private boolean ocrSettingsSimplifiedLayoutLandscape = Constants.DEFAULT_OCR_SETTINGS_SIMPLIFIED_LAYOUT_LANDSCAPE;
    private boolean ocrSettingsLargeNudgeButtons = Constants.DEFAULT_OCR_SETTINGS_LARGE_NUDGE_BUTTONS;
    private boolean ocrSettingsShowTextOrientationButton = Constants.DEFAULT_OCR_SETTINGS_SHOW_TEXT_ORIENTATION_BUTTON;
    private boolean ocrSettingsShowNudgeButtons = Constants.DEFAULT_OCR_SETTINGS_SHOW_NUDGE_BUTTONS;
    private boolean ocrSettingsShowSendButton = Constants.DEFAULT_OCR_SETTINGS_SHOW_SEND_BUTTON;
    private boolean ocrSettingsShowLookupNextButton = Constants.DEFAULT_OCR_SETTINGS_SHOW_LOOKUP_NEXT_BUTTON;
    private boolean ocrSettingsShowLookupPrevButton = Constants.DEFAULT_OCR_SETTINGS_SHOW_LOOKUP_PREV_BUTTON;

    private boolean ocrSettingsEdictCompactDefinitions = Constants.DEFAULT_OCR_SETTINGS_EDICT_COMPACT_DEFINITIONS;

    private String ocrSettingsEpwingDic1 = "";
    private String ocrSettingsEpwingDic2 = "";
    private String ocrSettingsEpwingDic3 = "";
    private String ocrSettingsEpwingDic4 = "";

    private boolean ocrSettingsEpwingCompactDefinitions = Constants.DEFAULT_OCR_SETTINGS_EPWING_COMPACT_DEFINITIONS;
    private int ocrSettingsEpwingMaxDefLines = Constants.DEFAULT_OCR_SETTINGS_EPWING_MAX_DEF_LINES;
    private boolean ocrSettingsEpwingParse = Constants.DEFAULT_OCR_SETTINGS_EPWING_PARSE;
    private boolean ocrSettingsEpwingShowExamples = Constants.DEFAULT_OCR_SETTINGS_EPWING_SHOW_EXAMPLES;
    private int ocrSettingsEpwingMaxExamples = Constants.DEFAULT_OCR_SETTINGS_EPWING_MAX_EXAMPLES;
    private boolean ocrSettingsEpwingCompactExamples = Constants.DEFAULT_OCR_SETTINGS_EPWING_COMPACT_EXAMPLES;
    private boolean ocrSettingsEpwingStripExamplesFromDefs = Constants.DEFAULT_OCR_SETTINGS_EPWING_STRIP_EXAMPLES_FROM_DEFS;

    private boolean ocrSettingsForceBorder = Constants.DEFAULT_OCR_SETTINGS_FORCE_BORDER;
    private boolean ocrSettingsShowFrequency = Constants.DEFAULT_OCR_SETTINGS_SHOW_FREQUENCY;
    private String ocrSettingsWordListSaveFilePath = Constants.DEFAULT_OCR_SETTINGS_WORD_LIST_SAVE_FILE_PATH;
    private String ocrSettingsWordListSaveFileFormat = Constants.DEFAULT_OCR_SETTINGS_WORD_LIST_SAVE_FILE_FORMAT;

    public OcrLayout(Context context) {
        super(context);
        this.init(context);
    }

    /** Constructor called by Android system at initialization. Also called when changing screen orientation. */
    public OcrLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.init(context);
    }

    /** Initialization routine called by the constructor. */
    private void init(Context context) {
        // Exit if in the Eclipse graphical editor
        if (this.isInEditMode()) {
            return;
        }

        this.context = context;

        this.readOcrSettings(context);

        this.determineScreenDimensions(context);

        File externalDir = this.context.getExternalFilesDir(null);
        this.tesseractDbDir = new File(externalDir.toString() + Constants.TESSERACT_ROOT_DIR);
        this.tesseractDbFile = new File(externalDir.toString() + Constants.TESSDATA_DIR,
                Constants.TESSERACT_DB_FILENAME);
        this.edictDbFile = new File(externalDir, Constants.EDICT_DB_FILENAME);
        this.namesDbFile = new File(externalDir, Constants.NAMES_DB_FILENAME);
        this.kanjiDbFile = new File(externalDir, Constants.KANJI_DB_FILENAME);
        this.kanjiDefFormatFile = new File(externalDir, Constants.KANJI_DEF_FORMAT_FILENAME);
        this.deinflectionDbFile = new File(externalDir, Constants.DEINFLECTION_DB_FILENAME);
        this.substitutionsDbFile = new File(externalDir, Constants.SUBSTITUTIONS_DB_FILENAME);
        this.freqDbFile = new File(externalDir, Constants.FREQ_DB_FILENAME);

        this.wordSetKnown = new WordSet(new File(externalDir, Constants.KNOWN_WORDS_FILENAME));
        this.wordSetTodo = new WordSet(new File(externalDir, Constants.TODO_WORDS_FILENAME));

        // Add OCR view
        this.ocrView = new OcrView(context);
        this.addView(this.ocrView);

        // Add dictionary view
        this.dicView = new WebView(context);
        this.dicView.setBackgroundColor(this.ocrSettingsDictBackgroundColor); // Make the background translucent.
        this.dicView.loadDataWithBaseURL(null, "<html><body></body></html>", "text/html", "utf-8", null);
        this.dicView.setVisibility(GONE);
        this.addView(this.dicView);

        // Set the path to eplkup
        ContextWrapper cw = new ContextWrapper(this.context);

        // Android 5.0 requires executables to be compiled with PIE (Position Independent Executable) support for security
        // reasons. However, PIE is not supported before Android 4.1, which is why we have 2 executables, one with
        // PIE and one without PIE.
        if (android.os.Build.VERSION.SDK_INT >= 20) {
            DicEpwing.setEplkupExe(cw.getApplicationInfo().dataDir + "/" + Constants.EPLKUP_FILENAME);
        } else {
            DicEpwing.setEplkupExe(cw.getApplicationInfo().dataDir + "/" + Constants.EPLKUP_NON_PIE_FILENAME);
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        this.btnSwapNudgeCorner = (Button) findViewById(R.id.btn_ocr_swap_nudge_corner);
        this.btnUp = (Button) findViewById(R.id.btn_ocr_up);
        this.btnDown = (Button) findViewById(R.id.btn_ocr_down);
        this.btnLeft = (Button) findViewById(R.id.btn_ocr_left);
        this.btnRight = (Button) findViewById(R.id.btn_ocr_right);
        this.btnTextOrientation = (Button) findViewById(R.id.btn_ocr_text_orientation);
        this.btnSend = (Button) findViewById(R.id.btn_ocr_send);
        this.btnLookupNext = (Button) findViewById(R.id.btn_ocr_lookup_next);
        this.btnLookupPrev = (Button) findViewById(R.id.btn_ocr_lookup_prev);
        this.btnMenu = (Button) findViewById(R.id.btn_ocr_menu);
        this.btnExit = (Button) findViewById(R.id.btn_ocr_exit);
        this.textViewStartMsg = (TextView) findViewById(R.id.btn_ocr_start_msg);

        // Store the original size of the nudge buttons
        LayoutParams params = (LayoutParams) btnSwapNudgeCorner.getLayoutParams();
        this.origNudgeButtonsWidth = params.width;
        this.origNudgeButtonsHeight = params.height;
    }

    /** Set size of nudge buttons based on user settings. */
    private void setNudgeButtonSize() {
        final float LARGE_SIZE_FACTOR = 1.3f;

        LayoutParams params;

        int newWidth = this.origNudgeButtonsWidth;
        int newHeight = this.origNudgeButtonsHeight;

        if (this.ocrSettingsLargeNudgeButtons) {
            newWidth *= LARGE_SIZE_FACTOR;
            newHeight *= LARGE_SIZE_FACTOR;
        }

        params = (LayoutParams) btnSwapNudgeCorner.getLayoutParams();
        params.width = newWidth;
        params.height = newHeight;
        this.btnSwapNudgeCorner.setLayoutParams(params);

        params = (LayoutParams) btnUp.getLayoutParams();
        params.width = newWidth;
        params.height = newHeight;
        this.btnUp.setLayoutParams(params);

        params = (LayoutParams) btnDown.getLayoutParams();
        params.width = newWidth;
        params.height = newHeight;
        this.btnDown.setLayoutParams(params);

        params = (LayoutParams) btnLeft.getLayoutParams();
        params.width = newWidth;
        params.height = newHeight;
        this.btnLeft.setLayoutParams(params);

        params = (LayoutParams) btnRight.getLayoutParams();
        params.width = newWidth;
        params.height = newHeight;
        this.btnRight.setLayoutParams(params);
    }

    /** Update properties for the text orientation menu item */
    public void updateMenuItemTextOrientation(MenuItem menuItem) {
        if (this.textOrientation == this.TEXT_ORIENTATION_HORIZONTAL) {
            menuItem.setTitle(getResources().getString(R.string.menu_item_ocr_text_orientation_horizontal));
        } else if (this.textOrientation == this.TEXT_ORIENTATION_VERTICAL) {
            menuItem.setTitle(getResources().getString(R.string.menu_item_ocr_text_orientation_vertical));
        } else // TEXT_ORIENTATION_AUTO
        {
            menuItem.setTitle(getResources().getString(R.string.menu_item_ocr_text_orientation_auto));
        }
    }

    /** Update properties for the send to menu item */
    public void updateMenuItemSend(MenuItem menuItem) {
        // No properties set at present
    }

    /** Show the OCR interface that appears after user has started to draw a box around text */
    public void showOcrInterface() {
        // Turn on GUI elements that depend on the existence of the capture box
        if (this.showSimplifiedInterface()) {
            this.btnMenu.setVisibility(VISIBLE);
            this.btnLookupNext.setVisibility(GONE);
            this.btnLookupPrev.setVisibility(GONE);
        } else // Full interface
        {
            this.btnMenu.setVisibility(GONE);

            if (this.ocrSettingsShowLookupNextButton) {
                this.btnLookupNext.setVisibility(VISIBLE);
            } else {
                this.btnLookupNext.setVisibility(INVISIBLE);
            }

            if (this.ocrSettingsShowLookupPrevButton) {
                this.btnLookupPrev.setVisibility(VISIBLE);
            } else {
                this.btnLookupPrev.setVisibility(INVISIBLE);
            }
        }

        this.btnExit.setVisibility(VISIBLE);
        this.setGuiButtonVisi(VISIBLE);
        this.dicView.setVisibility(VISIBLE);

        // Turn off initial message
        this.textViewStartMsg.setVisibility(GONE);
    }

    // For OnGestureListener. Called when user first touches the screen (but not as they are moving their finger)
    @Override
    public boolean onDown(MotionEvent e) {
        int x = (int) e.getX();
        int y = (int) e.getY();

        if (this.captureState == CaptureState.SET_TOP_LEFT) {
            // Set top-left corner of capture box to appear where the user touched.
            this.captureBox = new Rect(x, y, x + 1, y + 1);
            this.ocrView.setCaptureBox(this.captureBox);

            this.showOcrInterface();

            this.captureState = CaptureState.SET_BOTTOM_RIGHT;

            return true;
        } else if ((captureState == CaptureState.SET_BOTTOM_RIGHT) || (captureState == CaptureState.DRAG)) {
            this.dragRegion = this.determineDragRegion(x, y);
            this.captureState = CaptureState.DRAG;

            return true;
        }

        return false;
    }

    // For OnGestureListener.
    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    // For OnGestureListener.
    @Override
    public void onLongPress(MotionEvent e) {

    }

    // For OnGestureListener. Called after onDown() when the user moves finger on screen. 
    @Override
    public boolean onScroll(MotionEvent downEvent, MotionEvent dragEvent, float distanceX, float distanceY) {
        boolean handled = false;

        if (this.captureState == CaptureState.SET_BOTTOM_RIGHT) {
            this.captureBox.right -= (int) distanceX;
            this.captureBox.bottom -= (int) distanceY;
            this.ocrView.setCaptureBox(this.captureBox);

            handled = true;
        } else if (this.captureState == CaptureState.DRAG) {
            this.adjustCaptureBox(this.dragRegion, distanceX, distanceY);

            handled = true;
        }

        if (handled) {
            this.updateDicViewLocation();
        }

        return handled;
    }

    // For OnGestureListener.
    @Override
    public void onShowPress(MotionEvent e) {

    }

    // For OnGestureListener.
    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        // If the user has decided to tap the text instead of drawing a box around it, perform a trigger capture
        if ((this.captureState == CaptureState.SET_BOTTOM_RIGHT || captureState == CaptureState.DRAG)
                && (this.captureTimer != null)) {
            this.createTriggerCaptureBox(new Point((int) e.getX(), (int) e.getY()));
            this.updateDicViewLocation();
            this.ocrView.setCaptureBox(this.captureBox);
            this.forceCaptureUpdate();

            this.showOcrInterface();

            this.captureState = CaptureState.DRAG;
        }

        return true;
    }

    /** Provide the ComicView object to get screenshot from. */
    public void setComicView(ComicView comicView) {
        this.comicView = comicView;
    }

    /** Updates screenWidth and screenHeight. */
    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    private void determineScreenDimensions(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();

        if (android.os.Build.VERSION.SDK_INT >= 19) {
            // Get the total size of the screen (because on android 4.4 and
            // above we use immersive mode to hide the navigation bar).
            DisplayMetrics metrics = new DisplayMetrics();
            wm.getDefaultDisplay().getRealMetrics(metrics);

            this.screenWidth = metrics.widthPixels;
            this.screenHeight = metrics.heightPixels;
        } else if (android.os.Build.VERSION.SDK_INT >= 13) {
            // Get the size of the screen minus the height of the navigation bar
            Point size = new Point();
            display.getSize(size);

            this.screenWidth = size.x;
            this.screenHeight = size.y;
        } else {
            // Get the size of the screen minus the height of the navigation bar (old method)
            this.screenWidth = display.getWidth(); // deprecated
            this.screenHeight = display.getHeight(); // deprecated
        }
    }

    /** Should the simplified interface be shown? */
    private boolean showSimplifiedInterface() {
        boolean simplified = false;
        int screenOrientation = this.context.getResources().getConfiguration().orientation;

        if (screenOrientation == Configuration.ORIENTATION_PORTRAIT) {
            if (this.ocrSettingsSimplifiedLayoutPortrait) {
                simplified = true;
            }
        } else // ORIENTATION_LANDSCAPE
        {
            if (this.ocrSettingsSimplifiedLayoutLandscape) {
                simplified = true;
            }
        }

        return simplified;
    }

    /** Returns true is text capture box orientation is vertical. */
    private boolean isOrientationVertical(Rect captureBox) {
        if (this.textOrientation == this.TEXT_ORIENTATION_AUTO) {
            double aspectRatio = captureBox.width() / (double) captureBox.height();

            if (aspectRatio < 2.0) {
                return true;
            } else {
                return false;
            }
        } else {
            return (this.textOrientation == this.TEXT_ORIENTATION_VERTICAL);
        }
    }

    /** Set Tesseract text orientation */
    private void setOcrOrientation(Rect captureBox) {
        int orientation = this.textOrientation;

        if (orientation == this.TEXT_ORIENTATION_AUTO) {
            if (this.isOrientationVertical(captureBox)) {
                orientation = this.TEXT_ORIENTATION_VERTICAL;
            } else {
                orientation = this.TEXT_ORIENTATION_HORIZONTAL;
            }
        }

        if (orientation == this.TEXT_ORIENTATION_HORIZONTAL) {
            OcrLayout.tess.setPageSegMode(TessBaseAPI.PageSegMode.PSM_SINGLE_BLOCK);
        } else {
            OcrLayout.tess.setPageSegMode(TessBaseAPI.PageSegMode.PSM_SINGLE_BLOCK_VERT_TEXT);
        }
    }

    /** OCR lastCapture and update lastOcrText. The curCaptureBox argument is just used so that we can
        exit early if the user adjusts the capture box. */
    private boolean ocrCapture(Rect curCaptureBox) {
        if (this.lastCapture != null) {
            try {
                setOcrOrientation(curCaptureBox);

                // Give the captured bitmap to Tesseract
                OcrLayout.tess.setImage(this.lastCapture);

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    OcrLayout.tess.clear();
                    return false;
                }

                // Get the bounding rectangles
                this.boundingBoxes = OcrLayout.tess.getTextlines().getBoxRects();

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    OcrLayout.tess.clear();
                    return false;
                }

                this.lastOcrText = OcrLayout.tess.getUTF8Text().replaceAll("\n", "");

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    OcrLayout.tess.clear();
                    return false;
                }

                // Clears Tesseract's image and result data, but not the recognition data
                OcrLayout.tess.clear();

                // Correct common OCR mistakes
                this.lastOcrText = this.performOcrTextSubstitutions(this.lastOcrText);

                // Reset the prev/next history
                this.lookupWordIdxStack = new Stack<Integer>();
                this.lookupWordIdxStack.push(0);
            } catch (Exception e) {
                Log.e(LOG_TAG, "Exception in ocrCapture()! " + e);
                return false;
            }
        }

        return true;
    }

    /** Capture screen to the lastCapture bitmap. */
    private boolean captureScreen(Rect curCaptureBox) {
        View v = this.comicView;

        if ((v != null) && (curCaptureBox != null) && (curCaptureBox.width() > 0) && (curCaptureBox.height() > 0)) {
            this.clipOffset = new Point(0, 0);

            try {
                // Only create the bitmap that contains the entire screen once
                if (this.lastScreenshot == null) {
                    this.lastScreenshot = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
                    this.captureCanvas = new Canvas(this.lastScreenshot);
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                v.layout(0, 0, v.getWidth(), v.getHeight());

                // Draw the comic view to the lastScreenshot bitmap
                v.draw(this.captureCanvas);

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                // Get bitmap that contains only the capture box area
                Bitmap captureBmp = Bitmap.createBitmap(this.lastScreenshot, curCaptureBox.left, curCaptureBox.top,
                        curCaptureBox.width(), curCaptureBox.height());

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                // Create a Leptonica Pix object from the captured image
                this.lastCapture = ReadFile.readBitmap(captureBmp);

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                // Convert to grayscale
                this.lastCapture = Convert.convertTo8(this.lastCapture);

                if (this.lastCapture == null) {
                    return false;
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                Pix tempOtsu = Binarize.otsuAdaptiveThreshold(this.lastCapture, 2000, 2000, 0, 0, 0.0f);

                if (tempOtsu == null) {
                    return false;
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    tempOtsu.recycle();
                    return false;
                }

                /* Get the average intensity of the border pixels */
                float aveBorderBrightness = LeptUtils.getAveBorderBrightness(tempOtsu);
                tempOtsu.recycle();

                // If background is dark
                if (aveBorderBrightness <= 0.5f) {
                    // Negate image
                    boolean invertStatus = this.lastCapture.invert();

                    if (!invertStatus) {
                        return false;
                    }
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                // Scale the image
                this.lastCapture = Scale.scale(this.lastCapture, this.getIdealPreProcessingScaleFactor());

                if (this.lastCapture == null) {
                    return false;
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                // Apply unsharp mask
                this.lastCapture = Enhance.unsharpMasking(this.lastCapture, 5, 2.5f);

                if (this.lastCapture == null) {
                    return false;
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                // Binarize
                this.lastCapture = Binarize.otsuAdaptiveThreshold(this.lastCapture, 2000, 2000, 0, 0, 0.0f);

                if (this.lastCapture == null) {
                    return false;
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                // Remove furigana
                if (this.isOrientationVertical(this.captureBox)) {
                    Furigana.eraseFuriganaVerticalText(this.lastCapture, this.getIdealPreProcessingScaleFactor());
                } else {
                    Furigana.eraseFuriganaHorizontalText(this.lastCapture, this.getIdealPreProcessingScaleFactor());
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }

                // Clip foreground and add border
                if (this.isTriggerCapture || this.ocrSettingsForceBorder) {
                    // Are there any foreground pixels to clip to?
                    boolean canClip = Clip.testClipToForeground(this.lastCapture);

                    if (canClip) {
                        // Get the top-left point of the foreground clip (will be used to offset bounding boxes)
                        this.clipOffset = LeptUtils.getForegroundClipOffset(this.lastCapture);

                        // If user adjusted capture box since this routine was called, stop here
                        if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                            return false;
                        }

                        // Remove all existing white border pixels
                        this.lastCapture = Clip.clipToForeground(this.lastCapture);

                        if (this.lastCapture == null) {
                            return false;
                        }
                    }

                    // If user adjusted capture box since this routine was called, stop here
                    if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                        return false;
                    }

                    // Add an external white border
                    this.lastCapture = Pix.addBlackOrWhiteBorder(this.lastCapture, 2, 2, 2, 2, false);
                    this.clipOffset.x -= 2;
                    this.clipOffset.y -= 2;

                    this.isTriggerCapture = false;
                }

                // If user adjusted capture box since this routine was called, stop here
                if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(this.captureBox, curCaptureBox)) {
                    return false;
                }
            } catch (Exception e) {
                // If we're here, it's probably an out-of-memory exception
                Log.e(LOG_TAG, "Exception in captureScreen()! " + e);
                return false;
            }
        } else {
            return false;
        }

        return true;
    }

    /** Get the scale factor to scale the captured image such that the
     *  dimensions are equal to the un-scaled image size * SCALE_FACTOR. */
    public float getIdealPreProcessingScaleFactor() {
        ComicView v = this.comicView;
        float curZoom = v.getZoomFactor();
        float idealScaleFactor = OcrLayout.SCALE_FACTOR / curZoom;

        return idealScaleFactor;
    }

    /** Substitute common OCR mistakes with the correct text. */
    private String performOcrTextSubstitutions(String ocrText) {
        if (!this.substitutionsDbFile.exists()) {
            return ocrText;
        }

        // Populate the substitutions list if needed
        if (this.substitutionsList == null) {
            try {
                this.substitutionsList = new ArrayList<Pair<String, String>>();

                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(new FileInputStream(this.substitutionsDbFile), "UTF8"));

                String line = "";

                // Skip header line
                reader.readLine();

                while ((line = reader.readLine()) != null) {
                    // Skip comment lines and blank lines
                    if (line.startsWith("#") || (line.length() == 0)) {
                        continue;
                    }

                    String[] fields = line.split("=");

                    String from = "";
                    String to = "";

                    if (fields.length == 1) {
                        from = fields[0];
                        to = "";
                    } else if (fields.length == 2) {
                        from = fields[0];
                        to = fields[1];
                    } else {
                        continue;
                    }

                    // Replace tokens
                    from = from.replaceAll("%eq%", "=");
                    from = from.replaceAll("%perc%", "%");
                    to = to.replaceAll("%eq%", "=");
                    to = to.replaceAll("%perc%", "%");

                    Pair<String, String> fromTo = new Pair<String, String>(from, to);
                    this.substitutionsList.add(fromTo);
                }

                reader.close();
            } catch (Exception e) {
                // Don't care
            }
        }

        String correctedText = ocrText;

        // Make substitutions from substitutions file
        for (Pair<String, String> fromTo : this.substitutionsList) {
            correctedText = correctedText.replaceAll(fromTo.first, fromTo.second);
        }

        // Force a few substitutions
        correctedText = correctedText.replaceAll("[?'` ]", "").trim();

        if (correctedText.length() >= 1) {
            String firstChar = correctedText.substring(0, 1);
            String newFirstChar = firstChar;

            // Convert small hiragana to large hiragana
            if (newFirstChar.equals("??"))
                newFirstChar = "?";
            else if (newFirstChar.equals("?"))
                newFirstChar = "?";
            else if (newFirstChar.equals("?"))
                newFirstChar = "?";
            else if (newFirstChar.equals("?"))
                newFirstChar = "?";
            else if (newFirstChar.equals("?"))
                newFirstChar = "?";

            else if (newFirstChar.equals("?"))
                newFirstChar = "?";

            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";

            else if (newFirstChar.equals(""))
                newFirstChar = "?";

            // Convert small katakana to large katakana
            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";

            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";

            else if (newFirstChar.equals(""))
                newFirstChar = "";

            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";
            else if (newFirstChar.equals(""))
                newFirstChar = "";

            else if (newFirstChar.equals(""))
                newFirstChar = "";

            // Japanese words don't start with 
            else if (newFirstChar.equals(""))
                newFirstChar = "";

            // Convert prolonged sound mark to "ichi"
            else if (newFirstChar.equals(""))
                newFirstChar = "";

            correctedText = newFirstChar + correctedText.substring(1);
        }

        return correctedText;
    }

    /** Determine region that the provided coordinates are in relative to the capture box. */
    private DragRegion determineDragRegion(final int x, final int y) {
        final Rect r = this.captureBox;
        DragRegion dragRegion = DragRegion.NONE;

        if ((x < r.left) && (y < r.top)) {
            dragRegion = DragRegion.TOP_LEFT;
        } else if ((x > r.left) && (x < r.right) && (y < r.top)) {
            dragRegion = DragRegion.TOP;
        } else if ((x > r.right) && (y < r.top)) {
            dragRegion = DragRegion.TOP_RIGHT;
        } else if ((x < r.left) && (y > r.top) && (y < r.bottom)) {
            dragRegion = DragRegion.LEFT;
        } else if ((x > r.left) && (x < r.right) && (y > r.top) && (y < r.bottom)) {
            dragRegion = DragRegion.MIDDLE;
        } else if ((x > r.right) && (y > r.top) && (y < r.bottom)) {
            dragRegion = DragRegion.RIGHT;
        } else if ((x < r.left) && (y > r.bottom)) {
            dragRegion = DragRegion.BOTTOM_LEFT;
        } else if ((x > r.left) && (x < r.right) && (y > r.bottom)) {
            dragRegion = DragRegion.BOTTOM;
        } else if ((x > r.right) && (y > r.bottom)) {
            dragRegion = DragRegion.BOTTOM_RIGHT;
        }

        return dragRegion;
    }

    /** Adjust the specified region of the captureBox by the provided deltas. */
    public void adjustCaptureBox(final DragRegion dr, final float dx, final float dy) {
        switch (dr) {
        case TOP_LEFT:
            this.captureBox.left -= dx;
            this.captureBox.top -= dy;
            break;
        case TOP:
            this.captureBox.top -= dy;
            break;
        case TOP_RIGHT:
            this.captureBox.right -= dx;
            this.captureBox.top -= dy;
            break;
        case LEFT:
            this.captureBox.left -= dx;
            break;
        case MIDDLE:
            this.captureBox.left -= dx;
            this.captureBox.top -= dy;
            this.captureBox.right -= dx;
            this.captureBox.bottom -= dy;
            break;
        case RIGHT:
            this.captureBox.right -= dx;
            break;
        case BOTTOM_LEFT:
            this.captureBox.left -= dx;
            this.captureBox.bottom -= dy;
            break;
        case BOTTOM:
            this.captureBox.bottom -= dy;
            break;
        case BOTTOM_RIGHT:
            this.captureBox.right -= dx;
            this.captureBox.bottom -= dy;
            break;
        default:
            break;
        }

        // Bound the capture box to the edges of the screen and ensure that  
        // the bottom-right corner cannot be less than the top-right corner
        this.captureBox.left = MathUtils.bound(this.captureBox.left, 0, this.screenWidth - 1);
        this.captureBox.top = MathUtils.bound(this.captureBox.top, 0, this.screenHeight - 1);
        this.captureBox.right = MathUtils.bound(this.captureBox.right, this.captureBox.left + 1, this.screenWidth);
        this.captureBox.bottom = MathUtils.bound(this.captureBox.bottom, this.captureBox.top + 1,
                this.screenHeight);

        this.lastAdjustment = Calendar.getInstance();

        this.isTriggerCapture = false;

        // Remove bounding boxes from the OCR view until they are re calculated
        ocrView.setBoundingBoxes(null, 1.0f);

        // Pass the capture box to the OCR view to be drawn immediately
        this.ocrView.setCaptureBox(this.captureBox);
    }

    /** Nudge the capture box in the provided direction. */
    public void nudgeCaptureBox(NudgeDirection dir) {
        // Number of pixels to nudge by
        final int NUDGE_AMOUNT = 2;

        if (this.nudgeCorner == this.NUDGE_CORNER_BOTTOM_RIGHT) {
            if (dir == NudgeDirection.UP) {
                this.adjustCaptureBox(DragRegion.BOTTOM, 0, NUDGE_AMOUNT);
            } else if (dir == NudgeDirection.DOWN) {
                this.adjustCaptureBox(DragRegion.BOTTOM, 0, -NUDGE_AMOUNT);
            } else if (dir == NudgeDirection.LEFT) {
                this.adjustCaptureBox(DragRegion.RIGHT, NUDGE_AMOUNT, 0);
            } else // RIGHT
            {
                this.adjustCaptureBox(DragRegion.RIGHT, -NUDGE_AMOUNT, 0);
            }
        } else // TOP_LEFT
        {
            if (dir == NudgeDirection.UP) {
                this.adjustCaptureBox(DragRegion.TOP, 0, NUDGE_AMOUNT);
            } else if (dir == NudgeDirection.DOWN) {
                this.adjustCaptureBox(DragRegion.TOP, 0, -NUDGE_AMOUNT);
            } else if (dir == NudgeDirection.LEFT) {
                this.adjustCaptureBox(DragRegion.LEFT, NUDGE_AMOUNT, 0);
            } else // RIGHT
            {
                this.adjustCaptureBox(DragRegion.LEFT, -NUDGE_AMOUNT, 0);
            }
        }
    }

    /** Show the OCR Engine Load Failure dialog. */
    private void showOcrEngineLoadFailureDialog() {
        new AlertDialog.Builder(this.context).setIcon(android.R.drawable.ic_menu_info_details)
                .setTitle(R.string.ocr_engine_load_failed_dialog_title)
                .setMessage(R.string.ocr_engine_load_failed_dialog_msg)
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                    }
                }).show();
    }

    /** Show the OCR Dictionary Load Failure dialog. */
    private void showOcrDictLoadFailureDialog() {
        new AlertDialog.Builder(this.context).setIcon(android.R.drawable.ic_menu_info_details)
                .setTitle(R.string.ocr_dict_load_failed_dialog_title)
                .setMessage(R.string.ocr_dict_load_failed_dialog_msg)
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                    }
                }).show();
    }

    /** Show the OCR Assets Missing dialog. */
    private void showOcrAssetsMissingDialog() {
        new AlertDialog.Builder(this.context).setIcon(android.R.drawable.ic_menu_info_details)
                .setTitle(R.string.ocr_assets_missing_dialog_title)
                .setMessage(R.string.ocr_assets_missing_dialog_msg)
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                    }
                }).show();
    }

    /** Show the OCR Assets Missing dialog. */
    public void showErrorDialog(@StringRes int msgResId) {
        showErrorDialog(context.getString(msgResId));
    }

    public void showErrorDialog(String msg) {
        new AlertDialog.Builder(this.context).setIcon(android.R.drawable.ic_menu_info_details)
                .setTitle(R.string.ocr_error_dialog_title).setMessage(msg)
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                    }
                }).show();
    }

    /** Initialize the OCR engine. */
    private boolean initOcrEngine() {
        if (OcrLayout.tess == null) {
            OcrLayout.tess = new TessBaseAPI();

            try {
                boolean loaded = OcrLayout.tess.init(this.tesseractDbDir.toString(), "jpn");

                boolean status = true;
                status = status && OcrLayout.tess.setVariable("tessedit_enable_dict_correction", "1");
                status = status && OcrLayout.tess.setVariable("textord_really_old_xheight", "1");
                status = status && OcrLayout.tess.setVariable("tosp_threshold_bias2", "1");
                status = status && OcrLayout.tess.setVariable("classify_norm_adj_midpoint", "96");
                status = status && OcrLayout.tess.setVariable("tessedit_class_miss_scale", "0.002");
                status = status && OcrLayout.tess.setVariable("textord_initialx_ile", "1.0");
                status = status && OcrLayout.tess.setVariable("textord_min_linesize", "2.5");

                if (!status) {
                    Log.e(LOG_TAG, "initOcrEngine() setVariable failed!");
                    this.showOcrEngineLoadFailureDialog();
                    return false;
                }

                if (!loaded) {
                    Log.e(LOG_TAG, "initOcrEngine() load failed!");
                    this.showOcrEngineLoadFailureDialog();
                    return false;
                }
            } catch (Exception e) {
                Log.e(LOG_TAG, "initOcrEngine() exception!" + e);
                this.showOcrEngineLoadFailureDialog();
                return false;
            }

            OcrLayout.tess.setPageSegMode(TessBaseAPI.PageSegMode.PSM_SINGLE_BLOCK_VERT_TEXT);
        }

        return true;
    }

    /** Initialize the Japanese dictionary. */
    private boolean initJapaneseDictionary() {
        if (!OcrLayout.dicEdict.isDatabaseLoaded()) {
            // Setup the dictionary with paths to databases
            boolean loaded = OcrLayout.dicEdict.openDatabase(this.edictDbFile.getPath(),
                    this.deinflectionDbFile.getPath());

            if (!loaded) {
                Log.e(LOG_TAG, "initJapaneseDictionary() load failed!");
                this.showOcrDictLoadFailureDialog();
                return false;
            }
        }

        return true;
    }

    /** Initialize the Names dictionary. */
    private boolean initNamesDictionary() {
        if (!this.namesDbFile.exists()) {
            return false;
        }

        if (!OcrLayout.dicNames.isDatabaseLoaded()) {
            // Setup the dictionary with paths to databases
            boolean loaded = OcrLayout.dicNames.openDatabase(this.namesDbFile.getPath());

            if (!loaded) {
                Log.e(LOG_TAG, "initNamesDictionary() load failed!");
                this.showOcrDictLoadFailureDialog();
                return false;
            }
        }

        return true;
    }

    /** Initialize the Kanji dictionary. */
    private boolean initKanjiDictionary() {
        if (!this.kanjiDbFile.exists()) {
            return false;
        }

        if (!OcrLayout.dicKanji.isDatabaseLoaded()) {
            // Setup the dictionary with paths to databases
            boolean loaded = OcrLayout.dicKanji.openDatabase(this.kanjiDbFile.getPath());

            if (!loaded) {
                Log.e(LOG_TAG, "initKanjiDictionary() load failed!");
                this.showOcrDictLoadFailureDialog();
                return false;
            }
        }

        if (this.kanjiDefFormatFile.exists()) {
            try {
                BufferedReader reader = new BufferedReader(
                        new InputStreamReader(new FileInputStream(this.kanjiDefFormatFile), "UTF8"));

                String line = "";
                String defFormat = "";

                while ((line = reader.readLine()) != null) {
                    defFormat += line;
                }

                OcrLayout.dicKanji.setDefFormat(defFormat);

                reader.close();
            } catch (Exception e) {
                // Don't care
            }
        }

        return true;
    }

    /** Initialize the frequency database. */
    private boolean initFreqDatabase() {
        if (!OcrLayout.freqDb.isDatabaseLoaded()) {
            boolean loaded = OcrLayout.freqDb.openDatabase(this.freqDbFile.getPath());

            if (!loaded) {
                Log.e(LOG_TAG, "initFreqDatabase() load failed!");
                return false;
            }
        }

        return true;
    }

    /** Copy OCR assets to the application folder in external storage. 
     *  http://stackoverflow.com/questions/4447477/android-how-to-copy-files-in-assets-to-sdcard
     *  */
    private boolean copyOcrAssets() {
        AssetManager assetManager = this.context.getAssets();
        ContextWrapper cw = new ContextWrapper(this.context);
        String[] files = null;
        File externalDir = this.context.getExternalFilesDir(null);
        File tessdataDir = new File(externalDir.getPath() + Constants.TESSDATA_DIR);

        // Create the Tesseract directory structure if it doesn't exist
        if (!tessdataDir.exists()) {
            tessdataDir.mkdirs();
        }

        try {
            files = assetManager.list("");
        } catch (IOException e) {
            Log.e(LOG_TAG, "copyOcrAssets() failed to get asset file list!" + e);
            return false;
        }

        for (String filename : files) {
            InputStream in = null;
            OutputStream out = null;

            // Only store expected files
            if (filename.equals(Constants.EDICT_DB_FILENAME) || filename.equals(Constants.TESSERACT_DB_FILENAME)
                    || filename.equals(Constants.DEINFLECTION_DB_FILENAME)
                    || filename.equals(Constants.SUBSTITUTIONS_DB_FILENAME)
                    || filename.equals(Constants.EPLKUP_FILENAME)
                    || filename.equals(Constants.EPLKUP_NON_PIE_FILENAME)
                    || filename.equals(Constants.FREQ_DB_FILENAME)) {
                try {
                    in = assetManager.open(filename);

                    File outFile;

                    if (filename.equals(Constants.TESSERACT_DB_FILENAME)) {
                        outFile = new File(tessdataDir, filename);
                    } else if (filename.equals(Constants.EPLKUP_FILENAME)) {
                        outFile = new File(cw.getApplicationInfo().dataDir + "/" + Constants.EPLKUP_FILENAME);
                    } else if (filename.equals(Constants.EPLKUP_NON_PIE_FILENAME)) {
                        outFile = new File(
                                cw.getApplicationInfo().dataDir + "/" + Constants.EPLKUP_NON_PIE_FILENAME);
                    } else {
                        outFile = new File(externalDir, filename);
                    }

                    out = new FileOutputStream(outFile);
                    FileUtils.copyFile(in, out);
                    in.close();
                    in = null;
                    out.flush();
                    out.close();
                    out = null;

                    // Give eplkup execute permissions
                    if (filename.equals(Constants.EPLKUP_FILENAME)) {
                        ShellUtils.chmod(cw.getApplicationInfo().dataDir + "/" + Constants.EPLKUP_FILENAME, "744");
                    } else if (filename.equals(Constants.EPLKUP_NON_PIE_FILENAME)) {
                        ShellUtils.chmod(cw.getApplicationInfo().dataDir + "/" + Constants.EPLKUP_NON_PIE_FILENAME,
                                "744");
                    }
                } catch (IOException e) {
                    Log.e(LOG_TAG, "copyOcrAssets() copy failed!" + e);
                    return false;
                }
            }
        }

        return true;
    }

    /** Is this version of the app different than the stored version? */
    private boolean isVersionDifferent() {
        boolean versionDifferent = false;
        this.preferencesController = new PreferencesController(context);

        try {
            // Get the version of this app
            PackageInfo info = this.context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            String versionName = info.versionName;

            // Get the stored version preference
            String storedVersionName = this.preferencesController.getPreferences().getString("pref_stored_version",
                    "NONE");

            // If versions are different, store current app version
            if (!versionName.equals(storedVersionName)) {
                this.preferencesController.getPreferences().edit().putString("pref_stored_version", versionName)
                        .commit();
                versionDifferent = true;
            }
        } catch (NameNotFoundException e) {
            versionDifferent = true;
        }

        return versionDifferent;
    }

    /** Have the OCR assets been copied to external storage? */
    private boolean ocrAssetsExist() {
        return (this.tesseractDbFile.exists() && this.edictDbFile.exists() && this.deinflectionDbFile.exists()
                && this.freqDbFile.exists());
    }

    /** Check that the OCR database assets exist and are up to date. */
    private boolean needToUpdateOcrAssets() {
        boolean update = false;
        boolean versionDifferent = this.isVersionDifferent();

        if (!this.ocrAssetsExist() || versionDifferent) {
            update = true;
        }

        return update;
    }

    /** Start task that will show a progress dialog, copy OCR assets to external storage and prepare for capture. */
    private void runCopyAssetsTask() {
        /** Task responsible for showing a progress dialog and copying the OCR assets to external storage. */
        AsyncTask<Void, Void, Void> copyAssetsTask = new AsyncTask<Void, Void, Void>() {

            final Handler prepareCaptureHandler = new Handler();

            /** Runnable that will start the capture process */
            final Runnable prepareCaptureRunnable = new Runnable() {
                public void run() {
                    prepareCapture();
                }
            };

            @Override
            protected void onPreExecute() {
                assetCopyProgressDialog = new ProgressDialog(context);
                assetCopyProgressDialog.setTitle(getResources().getString(R.string.ocr_load_assets_dialog_title));
                assetCopyProgressDialog.setMessage(getResources().getString(R.string.ocr_load_assets_dialog_msg));
                assetCopyProgressDialog.setCancelable(false);
                assetCopyProgressDialog.setIndeterminate(true);
                assetCopyProgressDialog.show();
            }

            @Override
            protected Void doInBackground(Void... arg0) {
                copyOcrAssets();
                prepareCaptureHandler.post(prepareCaptureRunnable);
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
                if (assetCopyProgressDialog != null) {
                    assetCopyProgressDialog.dismiss();
                }
            }
        };

        copyAssetsTask.execute((Void[]) null);
    }

    public boolean createTriggerCaptureBox(Point pt) {
        final int IDEAL_CAP_WIDTH = 100;
        final int IDEAL_CAP_HALF_WIDTH = IDEAL_CAP_WIDTH / 2;
        final int IDEAL_CAP_HEIGHT = 450;
        final int IDEAL_CAP_ABOVE_PT = 35;
        final int IDEAL_LOOKAHEAD = 30;
        final int FALLBACK_WIDTH = 70;
        final int FALLBACK_HEIGHT = 200;
        final int FALLBACK_ABOVE_PT = 10;

        View v = this.comicView;

        if (v != null) {
            try {
                // Only create the bitmap that contains the entire screen once
                if (this.lastScreenshot == null) {
                    this.lastScreenshot = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
                    this.captureCanvas = new Canvas(this.lastScreenshot);
                }

                v.layout(0, 0, v.getWidth(), v.getHeight());

                // Draw the comic view to the lastScreenshot bitmap
                v.draw(this.captureCanvas);

                Rect cropRect = new Rect();

                if (this.textOrientation == TEXT_ORIENTATION_HORIZONTAL) {
                    cropRect.left = Math.max(0, pt.x - IDEAL_CAP_ABOVE_PT);
                    cropRect.top = Math.max(0, pt.y - IDEAL_CAP_HALF_WIDTH);
                    cropRect.right = Math.min(v.getWidth(), pt.x + IDEAL_CAP_HEIGHT);
                    cropRect.bottom = Math.min(v.getHeight(), pt.y + IDEAL_CAP_HALF_WIDTH);
                } else // Vertical or Auto
                {
                    cropRect.left = Math.max(0, pt.x - IDEAL_CAP_HALF_WIDTH);
                    cropRect.top = Math.max(0, pt.y - IDEAL_CAP_ABOVE_PT);
                    cropRect.right = Math.min(v.getWidth(), pt.x + IDEAL_CAP_HALF_WIDTH);
                    cropRect.bottom = Math.min(v.getHeight(), pt.y + IDEAL_CAP_HEIGHT);
                }

                // Get bitmap that contains the area to crop
                Bitmap cropBmp = Bitmap.createBitmap(this.lastScreenshot, cropRect.left, cropRect.top,
                        cropRect.width(), cropRect.height());

                // Get the click point relative to the cropped area
                Point ptInCropRect = new Point(pt.x - cropRect.left, pt.y - cropRect.top);

                // Crop
                Pix pixs = ReadFile.readBitmap(cropBmp);

                if (pixs == null) {
                    return false;
                }

                cropBmp.recycle();

                // Convert to grayscale
                pixs = Convert.convertTo8(pixs);

                if (pixs == null) {
                    return false;
                }

                Pix tempOtsu = Binarize.otsuAdaptiveThreshold(pixs, 2000, 2000, 0, 0, 0.0f);

                if (tempOtsu == null) {
                    return false;
                }

                float ave = 0.0f;

                // Get the average intensity of the pixels around the click point
                if (this.textOrientation == TEXT_ORIENTATION_HORIZONTAL) {
                    ave = Pix.averageInRect(tempOtsu, (int) (ptInCropRect.x * 0.9),
                            (int) (ptInCropRect.y - (tempOtsu.getHeight() * 0.95) / 2.0),
                            (int) (tempOtsu.getWidth() * 0.25), (int) (tempOtsu.getHeight() * 0.95));
                } else // Vertical or Auto
                {
                    ave = Pix.averageInRect(tempOtsu, (int) (ptInCropRect.x - (tempOtsu.getWidth() * 0.95) / 2.0),
                            (int) (ptInCropRect.y * 0.9), (int) (tempOtsu.getWidth() * 0.95),
                            (int) (tempOtsu.getHeight() * 0.25));
                }

                tempOtsu.recycle();

                // If background is dark
                if (ave >= 0.51f) {
                    // Negate image
                    boolean invertStatus = pixs.invert();

                    if (!invertStatus) {
                        return false;
                    }
                }

                // Blur to reduce noise
                pixs = Convolve.blockconvGray(pixs, 1, 1);

                if (pixs == null) {
                    return false;
                }

                // Apply unsharp mask
                pixs = Enhance.unsharpMasking(pixs, 5, 2.5f);

                if (pixs == null) {
                    return false;
                }

                // Binarize
                pixs = Binarize.otsuAdaptiveThreshold(pixs, 2000, 2000, 0, 0, 0.0f);

                if (pixs == null) {
                    return false;
                }

                // Remove black pixels connected to the border.
                // This eliminates annoying things like text bubbles.
                pixs = Seedfill.removeBorderConnComps(pixs, 8);

                if (pixs == null) {
                    return false;
                }

                // Find the black pixel closest to the click point
                Point nearestPixel = LeptUtils.findNearestBlackPixel(pixs, ptInCropRect.x, ptInCropRect.y, 40);

                // Get a bounding box surrounding the clicked text
                Rect boundingBox = BoundingTextRect.getBoundingRect(pixs, nearestPixel.x, nearestPixel.y,
                        (this.textOrientation != TEXT_ORIENTATION_HORIZONTAL), IDEAL_LOOKAHEAD);

                // Form the capture box size and position based on click point and bounding box
                this.captureBox = new Rect();
                this.captureBox.left = pt.x - ptInCropRect.x + boundingBox.left;
                this.captureBox.top = pt.y - ptInCropRect.y + boundingBox.top;
                this.captureBox.right = this.captureBox.left + boundingBox.width();
                this.captureBox.bottom = this.captureBox.top + boundingBox.height();

                // If could not find adequate bounding rectangle, fallback to a default size
                if (this.captureBox.width() <= 2 || this.captureBox.height() <= 2) {
                    if (this.textOrientation == TEXT_ORIENTATION_HORIZONTAL) {
                        this.captureBox = new Rect();
                        this.captureBox.left = Math.max(0, pt.x - FALLBACK_ABOVE_PT);
                        this.captureBox.top = Math.max(0, pt.y - FALLBACK_WIDTH / 2);
                        this.captureBox.right = Math.min(v.getWidth(), pt.x + FALLBACK_HEIGHT);
                        this.captureBox.bottom = Math.min(v.getHeight(), pt.y + FALLBACK_WIDTH / 2);
                    } else // Vertical or Auto
                    {
                        this.captureBox = new Rect();
                        this.captureBox.left = Math.max(0, pt.x - FALLBACK_WIDTH / 2);
                        this.captureBox.top = Math.max(0, pt.y - FALLBACK_ABOVE_PT);
                        this.captureBox.right = Math.min(v.getWidth(), pt.x + FALLBACK_WIDTH / 2);
                        this.captureBox.bottom = Math.min(v.getHeight(), pt.y + FALLBACK_HEIGHT);
                    }
                }

                pixs.recycle();
            } catch (Exception e) {
                // If we're here, it's probably an out-of-memory exception
                Log.e(LOG_TAG, "Exception in processTriggerCapture()! " + e);
                return false;
            }
        }

        this.isTriggerCapture = true;

        return true;
    }

    /** Perform a trigger capture. May be called from OcrLayout instead of start()
        to avoid being prompted to draw a box or to perform a trigger capture
        when already in OCR mode. */
    public void startTriggerCapture(ComicViewerActivity cva, Point pt) {
        /* If already in OCR mode, update the capture box location */
        if (this.captureTimer != null) {
            this.createTriggerCaptureBox(pt);
            this.updateDicViewLocation();
            this.ocrView.setCaptureBox(this.captureBox);
            this.forceCaptureUpdate();
        } else /* Start OCR mode */
        {
            this.start(cva);
            this.createTriggerCaptureBox(pt);
            this.updateDicViewLocation();
            this.ocrView.setCaptureBox(this.captureBox);
            this.forceCaptureUpdate();

            this.showOcrInterface();

            this.captureState = CaptureState.DRAG;
        }
    }

    /** Called when this view is shown. */
    public void start(ComicViewerActivity cva) {
        this.ocrStarted = false; // Will be set in prepareCapture()

        this.comicViewerActivity = cva;

        // Check if OCR assets need updating
        if (this.needToUpdateOcrAssets()) {
            this.runCopyAssetsTask();
        } else // Assets did not need to be updated
        {
            this.prepareCapture();
        }
    }

    /** Setup for the start of the capture. */
    private void prepareCapture() {
        if (!this.ocrAssetsExist()) {
            this.showOcrAssetsMissingDialog();
            this.comicViewerActivity.hideOcr();
            return;
        }

        if (!this.initOcrEngine()) {
            this.comicViewerActivity.hideOcr();
            return;
        }

        if (!this.initJapaneseDictionary()) {
            this.comicViewerActivity.hideOcr();
            return;
        }

        if (!this.initNamesDictionary()) {
            // Don't care
        }

        if (!this.initKanjiDictionary()) {
            // Don't care
        }

        if (!this.initFreqDatabase()) {
            // Don't care
        }

        this.readOcrSettings(this.context);

        this.setHideGui(false);

        this.textViewStartMsg.setVisibility(VISIBLE);

        // Remove GUI elements that depend on the existence of the capture box
        this.btnMenu.setVisibility(GONE);
        this.btnExit.setVisibility(GONE);
        this.btnLookupNext.setVisibility(GONE);
        this.btnLookupPrev.setVisibility(GONE);
        this.dicView.setVisibility(GONE);
        this.setGuiButtonVisi(GONE);

        // Set size of the nudge buttons
        this.setNudgeButtonSize();

        // Reset capture info
        this.captureState = CaptureState.SET_TOP_LEFT;
        this.captureBox = null;
        this.ocrView.setCaptureBox(null);
        this.boundingBoxes = null;
        this.ocrView.setBoundingBoxes(null, 1.0f);

        this.lastOcrText = "";
        this.lastEntryList = null;

        // Reset the prev/next history
        this.lookupWordIdxStack = new Stack<Integer>();
        this.lookupWordIdxStack.push(0);

        this.lastCaptureTime = Calendar.getInstance();
        this.lastAdjustment = Calendar.getInstance();

        // Start the timer that will schedule the capture and OCR
        this.captureTimer = new Timer("CaptureTimer", true);
        this.updateCaptureTask = new UpdateCaptureTask();
        this.captureTimer.scheduleAtFixedRate(this.updateCaptureTask, 10, 10);

        this.ocrStarted = true;
    }

    /** Called when this view is hidden. */
    public void stop() {
        try {
            if (!this.ocrStarted) {
                return;
            }

            // Stop the capture timer
            if (this.captureTimer != null) {
                this.captureTimer.cancel();
                this.captureTimer = null;
            }

            this.storeOcrSettings();
        } catch (Exception e) {
            Log.e(LOG_TAG, "Exception in stop()! " + e);
        }
    }

    /** Read in OCR-related settings. */
    private void readOcrSettings(Context context) {
        this.preferencesController = new PreferencesController(context);

        // Settings that can be set in the preferences activity
        this.ocrSettingsDictBackgroundColor = preferencesController.getPreferences()
                .getInt("ocr_settings_dict_background_color", Constants.DEFAULT_OCR_SETTINGS_DICT_BACKGROUND_COLOR);
        this.ocrSettingsDictExpressionColor = preferencesController.getPreferences()
                .getInt("ocr_settings_dict_expression_color", Constants.DEFAULT_OCR_SETTINGS_DICT_EXPRESSION_COLOR);
        this.ocrSettingsDictReadingColor = preferencesController.getPreferences()
                .getInt("ocr_settings_dict_reading_color", Constants.DEFAULT_OCR_SETTINGS_DICT_READING_COLOR);
        this.ocrSettingsDictDefinitionColor = preferencesController.getPreferences()
                .getInt("ocr_settings_dict_definition_color", Constants.DEFAULT_OCR_SETTINGS_DICT_DEFINITION_COLOR);
        this.ocrSettingsDictConjugationColor = preferencesController.getPreferences().getInt(
                "ocr_settings_dict_conjugation_color", Constants.DEFAULT_OCR_SETTINGS_DICT_CONJUGATION_COLOR);
        this.ocrSettingsDictSubDefColor = preferencesController.getPreferences()
                .getInt("ocr_settings_sub_def_color", Constants.DEFAULT_OCR_SETTINGS_DICT_SUB_DEF_COLOR);
        this.ocrSettingsDictExamplePrependColor = preferencesController.getPreferences().getInt(
                "ocr_settings_example_prepend_color", Constants.DEFAULT_OCR_SETTINGS_DICT_EXAMPLE_PREPEND_COLOR);
        this.ocrSettingsDictExampleJapColor = preferencesController.getPreferences()
                .getInt("ocr_settings_example_jap_color", Constants.DEFAULT_OCR_SETTINGS_DICT_EXAMPLE_JAP_COLOR);
        this.ocrSettingsDictExampleEngColor = preferencesController.getPreferences()
                .getInt("ocr_settings_example_eng_color", Constants.DEFAULT_OCR_SETTINGS_DICT_EXAMPLE_ENG_COLOR);
        this.ocrSettingsDictNameColor = preferencesController.getPreferences()
                .getInt("ocr_settings_dict_name_color", Constants.DEFAULT_OCR_SETTINGS_DICT_NAME_COLOR);
        this.ocrSettingsDictSeparatorColor = preferencesController.getPreferences()
                .getInt("ocr_settings_dict_separator_color", Constants.DEFAULT_OCR_SETTINGS_DICT_SEPARATOR_COLOR);
        this.ocrSettingsDictOcrTextColor = preferencesController.getPreferences()
                .getInt("ocr_settings_dict_ocr_text_color", Constants.DEFAULT_OCR_SETTINGS_DICT_OCR_TEXT_COLOR);
        this.ocrSettingsCaptureBoxColor = preferencesController.getPreferences()
                .getInt("ocr_settings_capture_box_color", Constants.DEFAULT_OCR_SETTINGS_CAPTURE_BOX_COLOR);
        this.ocrSettingsBoundingBoxColor = preferencesController.getPreferences()
                .getInt("ocr_settings_bounding_box_color", Constants.DEFAULT_OCR_SETTINGS_BOUNDING_BOX_COLOR);
        this.ocrSettingsFreqVeryCommonColor = preferencesController.getPreferences().getInt(
                "ocr_settings_freq_very_common_color", Constants.DEFAULT_OCR_SETTINGS_FREQ_VERY_COMMON_COLOR);
        this.ocrSettingsFreqCommonColor = preferencesController.getPreferences()
                .getInt("ocr_settings_freq_common_color", Constants.DEFAULT_OCR_SETTINGS_FREQ_COMMON_COLOR);
        this.ocrSettingsFreqUncommonColor = preferencesController.getPreferences()
                .getInt("ocr_settings_freq_uncommon_color", Constants.DEFAULT_OCR_SETTINGS_FREQ_UNCOMMON_COLOR);
        this.ocrSettingsFreqRareColor = preferencesController.getPreferences()
                .getInt("ocr_settings_freq_rare_color", Constants.DEFAULT_OCR_SETTINGS_FREQ_RARE_COLOR);
        this.ocrSettingsWordHighlightColor = preferencesController.getPreferences()
                .getInt("ocr_settings_word_highlight_color", Constants.DEFAULT_OCR_SETTINGS_WORD_HIGHLIGHT_COLOR);
        this.ocrSettingsKnownWordColor = preferencesController.getPreferences()
                .getInt("ocr_settings_known_word_color", Constants.DEFAULT_OCR_SETTINGS_KNOWN_WORD_COLOR);
        this.ocrSettingsShowBoundingBoxes = preferencesController.getPreferences()
                .getBoolean("ocr_settings_show_bounding_boxes", Constants.DEFAULT_OCR_SETTINGS_SHOW_BOUNDING_BOXES);

        if (this.ocrView != null) {
            this.ocrView.setCaptureBoxColor(this.ocrSettingsCaptureBoxColor);
            this.ocrView.setBoundingBoxColor(this.ocrSettingsBoundingBoxColor);
            this.ocrView.setBoundingBoxVisi(this.ocrSettingsShowBoundingBoxes);
        }

        this.ocrSettingsSimplifiedLayoutPortrait = preferencesController.getPreferences().getBoolean(
                "ocr_settings_simplified_layout_portrait",
                Constants.DEFAULT_OCR_SETTINGS_SIMPLIFIED_LAYOUT_PORTRAIT);
        this.ocrSettingsSimplifiedLayoutLandscape = preferencesController.getPreferences().getBoolean(
                "ocr_settings_simplified_layout_landscape",
                Constants.DEFAULT_OCR_SETTINGS_SIMPLIFIED_LAYOUT_LANDSCAPE);
        this.ocrSettingsLargeNudgeButtons = preferencesController.getPreferences()
                .getBoolean("ocr_settings_large_nudge_buttons", Constants.DEFAULT_OCR_SETTINGS_LARGE_NUDGE_BUTTONS);
        this.ocrSettingsShowTextOrientationButton = preferencesController.getPreferences().getBoolean(
                "ocr_settings_show_text_orientation_button",
                Constants.DEFAULT_OCR_SETTINGS_SHOW_TEXT_ORIENTATION_BUTTON);
        this.ocrSettingsShowNudgeButtons = preferencesController.getPreferences()
                .getBoolean("ocr_settings_show_nudge_buttons", Constants.DEFAULT_OCR_SETTINGS_SHOW_NUDGE_BUTTONS);
        this.ocrSettingsShowSendButton = preferencesController.getPreferences()
                .getBoolean("ocr_settings_show_send_button", Constants.DEFAULT_OCR_SETTINGS_SHOW_SEND_BUTTON);
        this.ocrSettingsShowLookupNextButton = preferencesController.getPreferences().getBoolean(
                "ocr_settings_show_lookup_next_button", Constants.DEFAULT_OCR_SETTINGS_SHOW_LOOKUP_NEXT_BUTTON);
        this.ocrSettingsShowLookupPrevButton = preferencesController.getPreferences().getBoolean(
                "ocr_settings_show_lookup_prev_button", Constants.DEFAULT_OCR_SETTINGS_SHOW_LOOKUP_PREV_BUTTON);

        this.ocrSettingsEdictCompactDefinitions = preferencesController.getPreferences().getBoolean(
                "ocr_settings_edict_compact_definitions", Constants.DEFAULT_OCR_SETTINGS_EDICT_COMPACT_DEFINITIONS);

        try {
            this.ocrSettingsEpwingMaxDefLines = preferencesController.getPreferences().getInt(
                    "ocr_settings_epwing_max_def_lines", Constants.DEFAULT_OCR_SETTINGS_EPWING_MAX_DEF_LINES);
        } catch (Exception e) {
            try {
                this.ocrSettingsEpwingMaxDefLines = Integer.parseInt(
                        preferencesController.getPreferences().getString("ocr_settings_epwing_max_def_lines",
                                String.valueOf(Constants.DEFAULT_OCR_SETTINGS_EPWING_MAX_DEF_LINES)));
            } catch (Exception e1) {
                this.ocrSettingsEpwingMaxDefLines = Constants.DEFAULT_OCR_SETTINGS_EPWING_MAX_DEF_LINES;
            }
        }

        this.ocrSettingsEpwingDic1 = preferencesController.getPreferences().getString("ocr_settings_epwing_path_1",
                "");
        this.ocrSettingsEpwingDic2 = preferencesController.getPreferences().getString("ocr_settings_epwing_path_2",
                "");
        this.ocrSettingsEpwingDic3 = preferencesController.getPreferences().getString("ocr_settings_epwing_path_3",
                "");
        this.ocrSettingsEpwingDic4 = preferencesController.getPreferences().getString("ocr_settings_epwing_path_4",
                "");

        this.ocrSettingsEpwingCompactDefinitions = preferencesController.getPreferences().getBoolean(
                "ocr_settings_epwing_compact_definitions",
                Constants.DEFAULT_OCR_SETTINGS_EPWING_COMPACT_DEFINITIONS);
        this.ocrSettingsEpwingParse = preferencesController.getPreferences().getBoolean("ocr_settings_epwing_parse",
                Constants.DEFAULT_OCR_SETTINGS_EPWING_PARSE);

        this.ocrSettingsEpwingShowExamples = preferencesController.getPreferences().getBoolean(
                "ocr_settings_epwing_show_examples", Constants.DEFAULT_OCR_SETTINGS_EPWING_SHOW_EXAMPLES);

        try {
            this.ocrSettingsEpwingMaxExamples = preferencesController.getPreferences()
                    .getInt("ocr_settings_epwing_max_examples", Constants.DEFAULT_OCR_SETTINGS_EPWING_MAX_EXAMPLES);
        } catch (Exception e) {
            try {
                this.ocrSettingsEpwingMaxExamples = Integer.parseInt(
                        preferencesController.getPreferences().getString("ocr_settings_epwing_max_examples",
                                String.valueOf(Constants.DEFAULT_OCR_SETTINGS_EPWING_MAX_EXAMPLES)));
            } catch (Exception e1) {
                this.ocrSettingsEpwingMaxExamples = Constants.DEFAULT_OCR_SETTINGS_EPWING_MAX_EXAMPLES;
            }
        }

        this.ocrSettingsEpwingCompactExamples = preferencesController.getPreferences().getBoolean(
                "ocr_settings_epwing_compact_examples", Constants.DEFAULT_OCR_SETTINGS_EPWING_COMPACT_EXAMPLES);
        this.ocrSettingsEpwingStripExamplesFromDefs = preferencesController.getPreferences().getBoolean(
                "ocr_settings_epwing_strip_examples_from_defs",
                Constants.DEFAULT_OCR_SETTINGS_EPWING_STRIP_EXAMPLES_FROM_DEFS);

        this.ocrSettingsForceBorder = preferencesController.getPreferences().getBoolean("ocr_settings_force_border",
                Constants.DEFAULT_OCR_SETTINGS_FORCE_BORDER);
        this.ocrSettingsShowFrequency = preferencesController.getPreferences()
                .getBoolean("ocr_settings_show_frequency", Constants.DEFAULT_OCR_SETTINGS_SHOW_FREQUENCY);
        this.ocrSettingsWordListSaveFilePath = preferencesController.getPreferences()
                .getString("ocr_settings_misc_word_list_save_file_path", "");
        this.ocrSettingsWordListSaveFileFormat = preferencesController.getPreferences().getString(
                "ocr_settings_misc_word_list_save_file_format", "Expression [tab] Reading [tab] Definition");

        // Settings that can't be set in the preferences activity
        this.setNudgeCorner(preferencesController.getPreferences().getInt("ocr_settings_nudge_corner",
                this.NUDGE_CORNER_BOTTOM_RIGHT));
        this.setTextOrientation(preferencesController.getPreferences().getInt("ocr_settings_text_orientation",
                this.TEXT_ORIENTATION_VERTICAL));
    }

    /** Store OCR-related settings. */
    private void storeOcrSettings() {
        this.preferencesController.getPreferences().edit().putInt("ocr_settings_nudge_corner", this.nudgeCorner)
                .commit();
        this.preferencesController.getPreferences().edit()
                .putInt("ocr_settings_text_orientation", this.textOrientation).commit();
    }

    /** Set visibility of all buttons (existing for the Hide/Show button) */
    private void setGuiButtonVisi(int visi) {
        if (this.ocrSettingsShowNudgeButtons) {
            this.btnSwapNudgeCorner.setVisibility(visi);
            this.btnUp.setVisibility(visi);
            this.btnDown.setVisibility(visi);
            this.btnLeft.setVisibility(visi);
            this.btnRight.setVisibility(visi);
        } else {
            this.btnSwapNudgeCorner.setVisibility(INVISIBLE);
            this.btnUp.setVisibility(INVISIBLE);
            this.btnDown.setVisibility(INVISIBLE);
            this.btnLeft.setVisibility(INVISIBLE);
            this.btnRight.setVisibility(INVISIBLE);
        }

        if (this.showSimplifiedInterface()) {
            // Remove button visibility for the other buttons
            visi = GONE;
        }

        if (this.ocrSettingsShowTextOrientationButton) {
            this.btnTextOrientation.setVisibility(visi);
        } else {
            this.btnTextOrientation.setVisibility(INVISIBLE);
        }

        if (this.ocrSettingsShowSendButton) {
            this.btnSend.setVisibility(visi);
        } else {
            this.btnSend.setVisibility(INVISIBLE);
        }

    }

    /** Toggle visibility of all OCR view GUI elements, with the exception of the Show/Hide button. */
    public void toggleHideGui() {
        this.setHideGui(!this.hideGui);
    }

    /** Hide/Show all OCR view GUI elements, with the exception of the Show/Hide button. */
    public void setHideGui(boolean hide) {
        this.hideGui = hide;
        int visi = GONE;

        if (hide) {
            visi = GONE;
        } else // Show GUI
        {
            visi = VISIBLE;
        }

        this.btnLookupNext.setVisibility(visi);
        this.btnLookupPrev.setVisibility(visi);
        this.ocrView.setVisibility(visi);
        this.dicView.setVisibility(visi);
        this.setGuiButtonVisi(visi);
    }

    /** Swap nudge corner between top-left and bottom-right. */
    public void swapNudgeCorner() {
        int newNudgeCorner = this.NUDGE_CORNER_BOTTOM_RIGHT;

        if (this.nudgeCorner == this.NUDGE_CORNER_BOTTOM_RIGHT) {
            newNudgeCorner = this.NUDGE_CORNER_TOP_LEFT;
        } else // Top-left
        {
            newNudgeCorner = this.NUDGE_CORNER_BOTTOM_RIGHT;
        }

        this.setNudgeCorner(newNudgeCorner);
    }

    /** Set the corner that the nudge buttons will adjust. */
    private void setNudgeCorner(int newNudgeCorner) {
        this.nudgeCorner = newNudgeCorner;

        if (this.btnSwapNudgeCorner == null) {
            return;
        }

        if (this.nudgeCorner == this.NUDGE_CORNER_BOTTOM_RIGHT) {
            this.btnSwapNudgeCorner.setText(R.string.btn_ocr_diag_bottom_right);
        } else // Top-left
        {
            this.btnSwapNudgeCorner.setText(R.string.btn_ocr_diag_top_left);
        }
    }

    /** Switch between vertical/horizontal/auto text orientation for OCR. */
    public void switchTextOrientation() {
        if (this.textOrientation == this.TEXT_ORIENTATION_HORIZONTAL) {
            this.setTextOrientation(this.TEXT_ORIENTATION_AUTO);
        } else if (this.textOrientation == this.TEXT_ORIENTATION_VERTICAL) {
            this.setTextOrientation(this.TEXT_ORIENTATION_HORIZONTAL);
        } else // TEXT_ORIENTATION_AUTO
        {
            this.setTextOrientation(this.TEXT_ORIENTATION_VERTICAL);
        }
    }

    /** Set text orientation. */
    private void setTextOrientation(int val) {
        this.textOrientation = val;

        if (this.btnTextOrientation == null) {
            return;
        }

        if (this.textOrientation == this.TEXT_ORIENTATION_HORIZONTAL) {
            this.btnTextOrientation.setText(getResources().getString(R.string.btn_ocr_text_orientation_horizontal));
        } else if (this.textOrientation == this.TEXT_ORIENTATION_VERTICAL) {
            this.btnTextOrientation.setText(getResources().getString(R.string.btn_ocr_text_orientation_vertical));
        } else // TEXT_ORIENTATION_AUTO
        {
            this.btnTextOrientation.setText(getResources().getString(R.string.btn_ocr_text_orientation_auto));
        }

        this.forceCaptureUpdate();
    }

    /** Get list of options to use with the OCR Send Dialog, */
    public List<String> createOcrSendList() {
        List<String> optList = new ArrayList<String>();

        // Add the options to the list
        optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_clipboard));
        optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_error_correction_editor));

        File wordListSaveFile = new File(this.ocrSettingsWordListSaveFilePath);

        if ((this.ocrSettingsWordListSaveFilePath.length() > 0) && wordListSaveFile.exists()
                && !wordListSaveFile.isDirectory()) {
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_word_list_save_file));
        }

        if (IntentUtils.isIntentAvailable(context, "sk.baka.aedict.action.ACTION_SEARCH_EDICT")) {
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_aedict));
        }

        if (IntentUtils.isIntentAvailable(context, "org.openintents.action.CREATE_FLASHCARD")) {
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_ankidroid));
        }

        if (IntentUtils.isIntentAvailable(context, "android.intent.action.VIEW")) {
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_eijiro_web));
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_goo_web));
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_sanseido_web));
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_yahoo_je_web));
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_yahoo_jj_web));
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_google_web));
            optList.add(this.context.getResources().getString(R.string.ocr_send_dialog_opt_google_images_web));
        }

        return optList;
    }

    /** Show the OCR Send Dialog. */
    public void showOcrSendDialog() {
        List<String> optList = createOcrSendList();
        CharSequence[] optCharSeq = optList.toArray(new CharSequence[optList.size()]);

        new AlertDialog.Builder(this.context).setIcon(android.R.drawable.ic_menu_info_details)
                .setTitle(R.string.ocr_send_dialog_title)
                .setItems(optCharSeq, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        sendOcrText(which);
                    }
                }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                    }
                }).show();
    }

    /** Send the OCR'd text to the provided destination. */
    public void sendOcrText(int sendDest) {
        List<String> optList = this.createOcrSendList();

        if (sendDest < optList.size()) {
            String sendDestStr = optList.get(sendDest);
            this.performSendAction(sendDestStr);
        }
    }

    public void performSendAction(String sendDestStr) {
        String textToSend = this.lastOcrText;
        Entry entry = null;

        /** If a word was found, use it */
        if ((this.lastEntryList != null) && (this.lastEntryList.size() > 0)) {
            entry = this.lastEntryList.get(0);
            textToSend = entry.Expression;
        }

        if ((entry != null) && sendDestStr
                .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_word_list_save_file))) {
            this.sendToWordListSaveFile(entry);
        } else if (textToSend.length() > 0) {
            // Determine the option that was selected
            if (sendDestStr.equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_clipboard))) {
                this.sendToClipboard(textToSend);
            } else if (sendDestStr.equals(
                    this.context.getResources().getString(R.string.ocr_send_dialog_opt_error_correction_editor))) {
                this.sendToErrorCorrectionDialog(this.lastOcrText); // Always edit the full OCR text
            } else if (sendDestStr
                    .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_aedict))) {
                this.sendToAedict(textToSend);
            } else if (sendDestStr
                    .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_ankidroid))) {
                if (entry != null) {
                    this.sendToAnkiDroid(entry);
                }
            } else if (sendDestStr
                    .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_eijiro_web))) {
                this.sendToEijiroWeb(textToSend);
            } else if (sendDestStr
                    .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_goo_web))) {
                this.sendToGooWeb(textToSend);
            } else if (sendDestStr
                    .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_sanseido_web))) {
                this.sendToSanseidoWeb(textToSend);
            } else if (sendDestStr
                    .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_yahoo_je_web))) {
                this.sendToYahooJeWeb(textToSend);
            } else if (sendDestStr
                    .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_yahoo_jj_web))) {
                this.sendToYahooJjWeb(textToSend);
            } else if (sendDestStr
                    .equals(this.context.getResources().getString(R.string.ocr_send_dialog_opt_google_web))) {
                this.sendToGoogleWeb(textToSend);
            } else if (sendDestStr.equals(
                    this.context.getResources().getString(R.string.ocr_send_dialog_opt_google_images_web))) {
                this.sendToGoogleImagesWeb(textToSend);
            }
        } else if (textToSend.length() == 0) {
            if (sendDestStr.equals(
                    this.context.getResources().getString(R.string.ocr_send_dialog_opt_error_correction_editor))) {
                this.sendToErrorCorrectionDialog(this.lastOcrText); // Always edit the full OCR text
            }
        }
    }

    /** Send the provided text to the clipboard. */
    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    private void sendToClipboard(String text) {
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB) {
            android.text.ClipboardManager clipboard = (android.text.ClipboardManager) this.context
                    .getSystemService(Context.CLIPBOARD_SERVICE);
            clipboard.setText(text);
        } else {
            android.content.ClipboardManager clipboard = (android.content.ClipboardManager) this.context
                    .getSystemService(Context.CLIPBOARD_SERVICE);
            android.content.ClipData clip = android.content.ClipData.newPlainText("OCR Manga Reader Text", text);
            clipboard.setPrimaryClip(clip);
        }
    }

    /** Send the provided text to the OCR Error Correction dialog. */
    private void sendToErrorCorrectionDialog(String text) {
        final EditText input = new EditText(this.context);
        input.setText(text);

        new AlertDialog.Builder(this.context)
                .setTitle(this.context.getResources().getString(R.string.ocr_error_correction_dialog_title))
                .setMessage(this.context.getResources().getString(R.string.ocr_error_correction_dialog_msg))
                .setView(input).setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {
                        lastOcrText = input.getText().toString();

                        // Reset the prev/next history
                        lookupWordIdxStack = new Stack<Integer>();
                        lookupWordIdxStack.push(0);

                        updateDicViewText();
                    }
                }).setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int whichButton) {

                    }
                }).show();
    }

    /** Save the entry to the word list save file. */
    private void sendToWordListSaveFile(Entry entry) {
        String entryText = "";

        if (this.ocrSettingsWordListSaveFileFormat.contains("Expression")) {
            entryText += entry.Expression + "\t";
        }

        if (this.ocrSettingsWordListSaveFileFormat.contains("Reading")) {
            entryText += entry.Reading + "\t";
        }

        if (this.ocrSettingsWordListSaveFileFormat.contains("Definition")) {
            entryText += entry.Definition + "\t";
        }

        if (this.ocrSettingsWordListSaveFileFormat.contains("Frequency")) {
            String freqStr = this.formatGlossFrequency(entry);
            freqStr = freqStr.replaceFirst("<span.*?>", "").replaceFirst("</span>", "").trim();

            entryText += freqStr;
        }

        entryText = entryText.trim();

        try {
            PrintWriter out = new PrintWriter(
                    new BufferedWriter(new FileWriter(this.ocrSettingsWordListSaveFilePath, true)));
            out.println(entryText);
            out.close();

            Toast.makeText(context, "Saved!", Toast.LENGTH_SHORT).show();
        } catch (IOException e) {
            Toast.makeText(context, "Save failed!", Toast.LENGTH_SHORT).show();
        }
    }

    /** Send the provided text to Aedict. */
    private void sendToAedict(String text) {
        if (IntentUtils.isIntentAvailable(context, "sk.baka.aedict.action.ACTION_SEARCH_EDICT")) {
            Intent intent = new Intent("sk.baka.aedict.action.ACTION_SEARCH_EDICT");
            intent.putExtra("kanjis", text);
            this.context.startActivity(intent);
        }
    }

    /**
     * @param entry
     * @author Marlon Paulse
     */
    private void sendToAnkiDroid(final Entry entry) {
        if (AnkiUtils.isApiAvailable(context)) {
            if (AnkiUtils.haveApiPermissions(context)) {
                ActivityCompat.requestPermissions(comicViewerActivity,
                        new String[] { AddContentApi.READ_WRITE_PERMISSION }, ANKI_RW_PERM_REQ_CODE);
                return;
            }

            String deckName = preferencesController.getPreferences()
                    .getString(PreferencesController.PREFERENCE_ANKI_DECK, AnkiUtils.getDefaultDeck());
            long deckId = AnkiUtils.getDeckID(deckName, context);
            if (deckId < 0) {
                showErrorDialog(context.getString(R.string.ocr_send_anki_deck_not_found) + " " + deckName);
                return;
            }

            final String modelName = preferencesController.getPreferences()
                    .getString(PreferencesController.PREFERENCE_ANKI_MODEL, AnkiUtils.getDefaultModel());
            long modelId = AnkiUtils.getModelID(modelName, context);
            if (modelId < 0) {
                showErrorDialog(context.getString(R.string.ocr_send_anki_model_not_found) + " " + modelName);
                return;
            }

            final String[] fields = AnkiUtils.getModelFields(modelId, context);
            if (fields == null) {
                showErrorDialog(context.getString(R.string.ocr_send_anki_model_fields_not_found) + " " + modelName);
                return;
            }

            String egSentence = "";
            if (entry.ExampleList != null && entry.ExampleList.size() > 0) {
                egSentence = entry.ExampleList.get(0).Text;
            }

            String[] info = { entry.Expression, entry.Reading, entry.Definition, "", egSentence };
            final String[] values = new String[fields.length];
            for (int i = 0; i < values.length; i++) {
                int fieldType = PreferencesController.PREFERENCE_ANKI_MODEL_FIELD_UNUSED_INT;
                try {
                    fieldType = Integer.parseInt(preferencesController.getPreferences().getString(
                            PreferencesController.PREFERENCE_ANKI_MODEL_FIELD_PREFIX + modelName + "_" + fields[i],
                            String.valueOf(PreferencesController.PREFERENCE_ANKI_MODEL_FIELD_UNUSED_INT)));
                } catch (NumberFormatException e) {
                }
                values[i] = (fieldType >= 0 && fieldType < info.length) ? info[fieldType] : "";
            }

            if (preferencesController.getPreferences()
                    .getBoolean(PreferencesController.PREFERENCE_ANKI_CONFIRM_SEND, true)) {
                AnkiSendDialogFragment dialog = AnkiSendDialogFragment.newInstance(deckId, deckName, modelId,
                        modelName, entry.Expression, fields, values);
                dialog.show(comicViewerActivity.getFragmentManager(), "ankiSend");
            } else {
                addAnkiCard(deckId, modelId, entry.Expression, values);
            }
        } else if (IntentUtils.isIntentAvailable(context, "org.openintents.action.CREATE_FLASHCARD")) {
            Intent intent = new Intent("org.openintents.action.CREATE_FLASHCARD");

            // String, language code of the first side
            intent.putExtra("SOURCE_LANGUAGE", "ja");

            //  String, language code of the second side
            intent.putExtra("TARGET_LANGUAGE", "en");

            // Text of the first side

            String sourceText = entry.Expression;
            String reading = entry.Reading;

            if (reading.length() > 0) {
                sourceText += " [" + reading + "]";
            }

            intent.putExtra("SOURCE_TEXT", sourceText);

            // Text of the second side
            intent.putExtra("TARGET_TEXT", entry.Definition);

            this.context.startActivity(intent);
        }
    }

    public void addAnkiCard(long deckId, long modelId, String modelKey, String[] fieldValues) {
        if (AnkiUtils.addCard(deckId, modelId, modelKey, fieldValues, context)) {
            Toast.makeText(context, R.string.ocr_send_anki_success, Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(context, R.string.ocr_send_anki_card_already_exists, Toast.LENGTH_SHORT).show();
        }
    }

    /** Send the provided text to Eijiro J-E Web. */
    private void sendToEijiroWeb(String text) {
        Intent intent = new Intent("android.intent.action.VIEW", Uri.parse("http://eow.alc.co.jp/" + text));
        this.context.startActivity(intent);
    }

    /** Send the provided text to goo jisho J-E/J-J Web. */
    private void sendToGooWeb(String text) {
        Intent intent = new Intent("android.intent.action.VIEW",
                Uri.parse("http://dictionary.goo.ne.jp/srch/all/" + text + "/m1u/"));
        this.context.startActivity(intent);
    }

    /** Send the provided text to Sanseido J-J Web. */
    private void sendToSanseidoWeb(String text) {
        Intent intent = new Intent("android.intent.action.VIEW",
                Uri.parse("http://www.sanseido.net/User/Dic/Index.aspx?TWords=" + text + "&st=0&DailyJJ=checkbox"));
        this.context.startActivity(intent);
    }

    /** Send the provided text to Yahoo Jisho J-E Web. */
    private void sendToYahooJeWeb(String text) {
        Intent intent = new Intent("android.intent.action.VIEW",
                Uri.parse("http://dic.search.yahoo.co.jp/dsearch?p=" + text + "&dic_id=ejje&stype=prefix"));
        this.context.startActivity(intent);
    }

    /** Send the provided text to Yahoo Jisho J-J Web */
    private void sendToYahooJjWeb(String text) {
        Intent intent = new Intent("android.intent.action.VIEW",
                Uri.parse("http://dic.search.yahoo.co.jp/dsearch?p=" + text + "&dic_id=jj&stype=prefix&b=1"));
        this.context.startActivity(intent);
    }

    /** Send the provided text to Google Web */
    private void sendToGoogleWeb(String text) {
        Intent intent = new Intent("android.intent.action.VIEW",
                Uri.parse("http://www.google.com/search?q=" + text + "&hl=en&lr=lang_ja"));
        this.context.startActivity(intent);
    }

    /** Send the provided text to Google Images J-J Web */
    private void sendToGoogleImagesWeb(String text) {
        Intent intent = new Intent("android.intent.action.VIEW",
                Uri.parse("http://images.google.com/images?q=" + text + "&hl=en"));
        this.context.startActivity(intent);
    }

    /** Force the timer task to perform a capture and OCR. */
    private void forceCaptureUpdate() {
        if (this.captureBox != null) {
            this.forceUpdate = true; // Note: will be unset by the timer task
        }
    }

    /** Determine the quadrant of the screen that the provided coordinates are in. */
    private Quadrant determineQuadrant(int x, int y) {
        Quadrant quadrant = Quadrant.BOTTOM_LEFT;

        int centerX = this.screenWidth / 2;
        int centerY = this.screenHeight / 2;

        // If left half
        if (x <= centerX) {
            if (y <= centerY) // Top half
            {
                quadrant = Quadrant.TOP_LEFT;
            } else // Bottom half
            {
                quadrant = Quadrant.BOTTOM_LEFT;
            }
        } else // Right half
        {
            if (y <= centerY) // Top half
            {
                quadrant = Quadrant.TOP_RIGHT;
            } else // Bottom half
            {
                quadrant = Quadrant.BOTTOM_RIGHT;
            }
        }

        return quadrant;
    }

    /** Determine dictionary view location based on the capture box location and size. */
    private DicViewLocation determineDicViewLocation() {
        Rect r = this.captureBox;

        DicViewLocation loc = DicViewLocation.BOTTOM_LEFT;

        Quadrant qP1 = this.determineQuadrant(r.left, r.top);
        Quadrant qP2 = this.determineQuadrant(r.right, r.bottom);

        //    (qP1 == Quadrant.TOP_RIGHT)   

        if (qP1 == Quadrant.TOP_LEFT) {
            if (qP2 == Quadrant.TOP_LEFT) {
                loc = DicViewLocation.BOTTOM;
            } else if (qP2 == Quadrant.TOP_RIGHT) {
                loc = DicViewLocation.BOTTOM;
            } else if (qP2 == Quadrant.BOTTOM_LEFT) {
                loc = DicViewLocation.RIGHT;
            } else // BOTTOM_RIGHT
            {
                // Capture box spans all quadrants, so choose location least likely to interfere.
                loc = DicViewLocation.TOP;
            }
        } else if (qP1 == Quadrant.TOP_RIGHT) {
            loc = DicViewLocation.LEFT;
        } else if (qP1 == Quadrant.BOTTOM_LEFT) {
            loc = DicViewLocation.TOP;
        } else // BOTTOM_RIGHT
        {
            loc = DicViewLocation.TOP;
        }

        return loc;
    }

    /** Convert a DicViewLocation to a Rect. */
    private Rect dicViewLocationToRect(DicViewLocation loc) {
        Rect r;

        int sw = this.screenWidth;
        int sh = this.screenHeight;
        int cx = sw / 2;
        int cy = sh / 2;

        switch (loc) {
        case TOP_LEFT:
            r = new Rect(0, 0, cx, cy);
            break;
        case TOP:
            r = new Rect(0, 0, sw, cy);
            break;
        case TOP_RIGHT:
            r = new Rect(cx, 0, sw, cy);
            break;
        case LEFT:
            r = new Rect(0, 0, cx, sh);
            break;
        case RIGHT:
            r = new Rect(cx, 0, sw, sh);
            break;
        case BOTTOM_LEFT:
            r = new Rect(0, cy, cx, sh);
            break;
        case BOTTOM:
            r = new Rect(0, cy, sw, sh);
            break;
        case BOTTOM_RIGHT:
            r = new Rect(cx, cy, sw, sh);
            break;
        default:
            r = new Rect(0, cy, cx, sh);
            break;
        }

        return r;
    }

    /** Update the location of the dictionary view based on the location of the capture box. */
    @SuppressLint("NewApi")
    public void updateDicViewLocation() {
        if (this.captureBox != null) {
            DicViewLocation loc = this.determineDicViewLocation();
            Rect rect = this.dicViewLocationToRect(loc);

            int width = rect.width();
            int height = rect.height();

            LayoutParams layoutParams = new LayoutParams(width, height);

            if (android.os.Build.VERSION.SDK_INT >= 11) {
                this.dicView.setLayoutParams(layoutParams);
                this.dicView.setX(rect.left);
                this.dicView.setY(rect.top);
            } else {
                layoutParams.leftMargin = rect.left;
                layoutParams.topMargin = rect.top;
                this.dicView.setLayoutParams(layoutParams);
            }
        }
    }

    /** Add EPWING dictionary to the provided list based on the provided CATALOGS file */
    private void addEpwingDicToListIfValid(List<Dic> dicList, String catalogsFile) {
        if (catalogsFile.length() > 0) {
            File file = new File(catalogsFile);

            if (file.exists()) {
                Dic tempDic = null;

                try {
                    if (ocrSettingsEpwingParse) {
                        tempDic = DicEpwing.createEpwingDic(catalogsFile);
                    } else {
                        tempDic = new DicEpwingRaw(catalogsFile, 0);
                    }
                } catch (Exception e) {
                    Log.e(LOG_TAG, "Exception in addEpwingDicToListIfValid()! " + e);
                }

                if (tempDic != null) {
                    dicList.add(tempDic);
                }
            }
        }
    }

    /** Get list containing EPWING dictionaries */
    private List<Dic> getEpwingDicList() {
        List<Dic> dicList = new LinkedList<Dic>();

        this.addEpwingDicToListIfValid(dicList, this.ocrSettingsEpwingDic1);
        this.addEpwingDicToListIfValid(dicList, this.ocrSettingsEpwingDic2);
        this.addEpwingDicToListIfValid(dicList, this.ocrSettingsEpwingDic3);
        this.addEpwingDicToListIfValid(dicList, this.ocrSettingsEpwingDic4);

        return dicList;
    }

    /** After the text has been OCR'd, this may be invoked to advance to the next word. */
    public void lookupNextWord() {
        try {
            // If the current word is in the dictionary, advance to the next word
            if ((this.lastEntryList != null) && (this.lastEntryList.size() > 0)) {
                String curWord = this.lastEntryList.get(0).Inflected;
                Integer newIdx = this.lookupWordIdxStack.peek() + curWord.length();

                if (newIdx < this.lastOcrText.length()) {
                    this.lookupWordIdxStack.push(newIdx);
                    this.updateDicViewText();
                }
            } else // Current word is not in the dictionary, advance one character
            {
                Integer newIdx = this.lookupWordIdxStack.peek() + 1;

                if (newIdx < this.lastOcrText.length()) {
                    this.lookupWordIdxStack.push(newIdx);
                    this.updateDicViewText();
                }
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "Exception in lookupPrevWord()! " + e);
        }
    }

    /** After the text has been OCR'd, this may be invoked to go back to the previous word. */
    public void lookupPrevWord() {
        try {
            // If we can go back, then go back
            if (this.lookupWordIdxStack.size() > 1) {
                this.lookupWordIdxStack.pop();
                this.updateDicViewText();
            }
        } catch (Exception e) {
            Log.e(LOG_TAG, "Exception in lookupPrevWord()! " + e);
        }
    }

    /** Update dictionary view with a lookup of lastOcrText. */
    private void updateDicViewText() {
        try {
            final String ocrPrefix = getResources().getString(R.string.ocr_prefix);
            final String noResultsMsg = getResources().getString(R.string.ocr_no_results);
            final String noEntriesMsg = getResources().getString(R.string.ocr_no_dict_entries);
            final String ocrTextHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictOcrTextColor));
            final String separatorHtmlColor = String.format("#%06X",
                    (0xFFFFFF & this.ocrSettingsDictSeparatorColor));
            final String highlightColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsWordHighlightColor));

            String htmlText = String.format("<html><body><span lang='ja' style='color: %s'>%s ", ocrTextHtmlColor,
                    ocrPrefix);

            // If no OCR results were found
            if (this.lastOcrText.length() == 0) {
                htmlText += noResultsMsg + "</span></body></html>";
            } else // OCR results were found
            {
                int curRawLookupIdx = this.lookupWordIdxStack.peek();
                String curRawLookup = this.lastOcrText.substring(curRawLookupIdx);

                this.lastEntryList = new ArrayList<Entry>();

                // Lookup first word in dictionary
                List<Entry> edictEntryList = OcrLayout.dicEdict.searchWord(curRawLookup, 0);

                if (edictEntryList == null) {
                    edictEntryList = new ArrayList<Entry>();
                }

                // Add entries from each EPWING dictionary to epwingEntryList
                List<Entry> epwingEntryList = new ArrayList<Entry>();

                if (edictEntryList.size() > 0) {
                    String firstWord = "";

                    // If the first captured word is all kana, don't lookup the kanjified form of the word.
                    // Example1: if ? is highlighted, use ? rather than the kanji equivalent ()
                    // Example2: if ?? is highlighted, use ? rather then   
                    if (!UtilsLang.containsIdeograph(edictEntryList.get(0).Inflected)) {
                        firstWord = edictEntryList.get(0).Reading;
                    } else {
                        firstWord = edictEntryList.get(0).Expression;
                    }

                    List<Dic> epwingDicList = this.getEpwingDicList();

                    FineTune fineTune = new FineTune();
                    fineTune.JjKeepExamplesInDef = !this.ocrSettingsEpwingStripExamplesFromDefs;

                    // For each EPWING dictionary
                    for (Dic dic : epwingDicList) {
                        if (dic instanceof DicEpwing) {
                            try {
                                List<Entry> entryList = dic.lookup(firstWord, true, fineTune);

                                if (entryList != null) {
                                    epwingEntryList.addAll(entryList);
                                }
                            } catch (Exception e) {
                                Log.e(LOG_TAG, "Exception in updateDicViewText() when searching EPWING dics! " + e);
                            }
                        }
                    }

                    if (epwingEntryList.size() > 0) {
                        // Save Inflected from the first Edict entry into the first EPWING entry. 
                        // It's used for next/previous word functionality.
                        epwingEntryList.get(0).Inflected = edictEntryList.get(0).Inflected;
                    }
                }

                this.lastEntryList.addAll(epwingEntryList);
                this.lastEntryList.addAll(edictEntryList);

                List<Entry> namesList = null;

                // Lookup names and add them to lastEntryList 
                if (OcrLayout.dicNames.isDatabaseLoaded()) {
                    namesList = OcrLayout.dicNames.searchWord(curRawLookup, 10);

                    if ((namesList != null) && (namesList.size() != 0)) {
                        if (this.lastEntryList.size() == 0) {
                            this.lastEntryList = namesList;
                        } else {
                            this.lastEntryList.addAll(namesList);
                        }
                    }
                }

                List<Entry> kanjiList = null;

                // Lookup kanji and add them to lastEntryList 
                if (OcrLayout.dicKanji.isDatabaseLoaded()) {
                    kanjiList = OcrLayout.dicKanji.lookup(curRawLookup, false, null);

                    if ((kanjiList != null) && (kanjiList.size() != 0)) {
                        if (this.lastEntryList.size() == 0) {
                            this.lastEntryList = kanjiList;
                        } else {
                            this.lastEntryList.addAll(kanjiList);
                        }
                    }
                }

                // If no dictionary entries were found
                if (this.lastEntryList.size() == 0) {
                    String leadingText = this.lastOcrText.substring(0, curRawLookupIdx);
                    String highlightedWord = this.lastOcrText.substring(curRawLookupIdx, curRawLookupIdx + 1);
                    String trailingText = this.lastOcrText.substring(curRawLookupIdx + 1);

                    htmlText += String.format(
                            "%s<span lang='ja' style='background-color: %s'>%s</span>%s</span><br />", leadingText,
                            highlightColor, highlightedWord, trailingText);

                    htmlText += String.format("<span style='color:LightGray'>%s</ span>", noEntriesMsg);
                } else // One or more dictionary entries were found
                {
                    String leadingText = this.lastOcrText.substring(0, curRawLookupIdx);
                    String highlightedWord = this.lastOcrText.substring(curRawLookupIdx,
                            curRawLookupIdx + this.lastEntryList.get(0).Inflected.length());
                    String trailingText = this.lastOcrText
                            .substring(curRawLookupIdx + this.lastEntryList.get(0).Inflected.length());

                    htmlText += String.format(
                            "%s<span lang='ja' style='background-color: %s'>%s</span>%s</span><br />", leadingText,
                            highlightColor, highlightedWord, trailingText);

                    // Add each entry to the HTML
                    for (int i = 0; i < this.lastEntryList.size(); i++) {
                        String entryText = this.entryToHtml(this.lastEntryList.get(i));
                        htmlText += entryText;

                        // If this is not the final entry, add a line
                        if (i != (this.lastEntryList.size() - 1)) {
                            htmlText += String.format("<hr style='border-color: %s'>", separatorHtmlColor);
                        }
                    }
                }

                htmlText += "</body></html>";
            }

            this.dicView.setBackgroundColor(this.ocrSettingsDictBackgroundColor);
            this.dicView.loadDataWithBaseURL(null, htmlText, "text/html", "utf-8", null);
        } catch (Exception e) {
            Log.e(LOG_TAG, "Exception in updateDicViewText()! " + e);
        }
    }

    /** Format marker for known/todo words */
    private String formatKnownWordMarker(Entry entry) {
        String knownWordColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsKnownWordColor));

        String markerText = "";

        if (this.wordSetKnown.isWordInSet(entry.Expression)) {
            markerText = "*";
        } else if (this.wordSetTodo.isWordInSet(entry.Expression)) {
            markerText = "*t";
        } else if (this.wordSetKnown.isWordInSet(entry.Reading)) {
            markerText = "*r";
        } else if (this.wordSetTodo.isWordInSet(entry.Reading)) {
            markerText = "*tr";
        }

        String html = String.format(Locale.US, " <span style='color: %s'>%s</span>", knownWordColor, markerText);

        return html;
    }

    /** Format a frequency for the gloss. */
    private String formatGlossFrequency(Entry entry) {
        String freq = "";
        boolean freqBasedOnReading = false;
        int freqNum = -1;

        // Get all of the expression/reading pairs
        List<Entry> combos = UtilsCommon.getExpressionReadingCombinations(entry.Expression, entry.Reading);

        // Use the frequency of the first expression that has frequency information
        for (Entry comboEntry : combos) {
            int readingFreqNum = OcrLayout.freqDb.getFrequency(comboEntry.Reading);
            boolean readingSameAsExpression = comboEntry.Expression.equals(comboEntry.Reading);
            int expressionFreqNum = readingFreqNum;

            if (!readingSameAsExpression) {
                expressionFreqNum = OcrLayout.freqDb.getFrequency(comboEntry.Expression);
            }

            // If neither the reading nor the expression is in the freq db
            if ((expressionFreqNum == -1) && (readingFreqNum == -1)) {
                continue;
            }

            // If the highlighted word does contain kanji
            if (!readingSameAsExpression && !UtilsLang.containsIdeograph(entry.Inflected) && (readingFreqNum != -1)
                    && (OcrLayout.dicEdict.getReadingCount(comboEntry.Reading) == 1)) {
                freqNum = readingFreqNum;
                freqBasedOnReading = true;
                break;
            }

            if (readingSameAsExpression && (readingFreqNum != -1)) {
                freqNum = readingFreqNum;
                break;
            }

            // If the expression is in the freq db
            if (expressionFreqNum != -1) {
                freqNum = expressionFreqNum;
                break;
            }

            // If the reading is in the freq db
            if (readingFreqNum != -1) {
                freqNum = readingFreqNum;
                freqBasedOnReading = true;
                break;
            }
        }

        // If frequency was found
        if (freqNum != -1) {
            String veryCommonColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsFreqVeryCommonColor));
            String commonColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsFreqCommonColor));
            String uncommonColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsFreqUncommonColor));
            String rareColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsFreqRareColor));

            // Determine style to use
            String freqColor = rareColor;

            if (freqNum <= 5000) {
                freqColor = veryCommonColor;
            } else if (freqNum <= 10000) {
                freqColor = commonColor;
            } else if (freqNum <= 20000) {
                freqColor = uncommonColor;
            }

            // If frequency was based on the reading
            if (freqBasedOnReading) {
                // Indicate that frequency was based on the reading by adding "_r" to the end of the frequency
                freq = String.format(Locale.US, " <span style='color: %s'>%d_r</span>", freqColor, freqNum);
            } else {
                freq = String.format(Locale.US, " <span style='color: %s'>%s</span>", freqColor, freqNum);
            }
        }

        return freq;
    }

    /** Convert dictionary entry to HTML. */
    private String entryToHtml(Entry entry) {
        String html = "";
        String expressionHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictExpressionColor));
        String readingHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictReadingColor));
        String conjugationHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictConjugationColor));
        String definitionHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictDefinitionColor));
        String subdefHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictSubDefColor));
        String exPrependHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictExamplePrependColor));
        String exJapHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictExampleJapColor));
        String exEngHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictExampleEngColor));
        String dicNameHtmlColor = String.format("#%06X", (0xFFFFFF & this.ocrSettingsDictNameColor));

        boolean isEpwing = (entry.SourceDic instanceof DicEpwing);
        boolean isEpwingRaw = (entry.SourceDic instanceof DicEpwingRaw);

        if (isEpwingRaw) {
            if ((entry.SourceDic != null) && (entry.SourceDic.ShortName.length() != 0)) {
                html += String.format(" <span style='color: %s'>%s?</span>", dicNameHtmlColor,
                        entry.SourceDic.ShortName);
                html += "<br />";
            }
        } else // EDICT or names or parsed EPWING dictionary
        {
            html += String.format("<span lang='ja' style='color: %s'>%s</span>", expressionHtmlColor,
                    entry.Expression);

            if (entry.Reading.length() != 0) {
                html += String.format(" <span lang='ja' style='color: %s'>%s</span>", readingHtmlColor,
                        entry.Reading);
            }

            html += this.formatKnownWordMarker(entry);

            if (this.ocrSettingsShowFrequency) {
                html += this.formatGlossFrequency(entry);
            }

            if (entry.DeinflectionRule.length() != 0) {
                html += String.format(" <span style='color: %s'>(%s)</span>", conjugationHtmlColor,
                        entry.DeinflectionRule);
            }

            if ((entry.SourceDic != null) && (entry.SourceDic.ShortName.length() != 0)) {
                html += String.format(" <span style='color: %s'>%s?</span>", dicNameHtmlColor,
                        entry.SourceDic.ShortName);
            }

            html += "<br />";
        }

        // Format definition
        String definition = entry.Definition;

        if (isEpwing) {
            // If the definition is blank (for example  in Ken5), set the definition to the first example sentence
            if (definition.trim().length() == 0) {
                if (entry.ExampleList.size() > 0) {
                    definition = this.formatSingleExample(entry.ExampleList.get(0), exJapHtmlColor, exEngHtmlColor,
                            exPrependHtmlColor);

                    // Remove this example so that it won't be used again
                    entry.ExampleList.remove(0);
                }
            }

            String[] defLines = definition.split("<br />");

            if (defLines.length > ocrSettingsEpwingMaxDefLines) {
                definition = "";

                for (int lineIdx = 0; lineIdx < ocrSettingsEpwingMaxDefLines; lineIdx++) {
                    definition += defLines[lineIdx];

                    if (lineIdx != ocrSettingsEpwingMaxDefLines - 1) {
                        definition += "<br />";
                    }
                }
            }

            if (ocrSettingsEpwingCompactDefinitions) {
                definition = definition.replaceAll("<br />", " ");
            }
        } else {
            if (!this.ocrSettingsEdictCompactDefinitions) {
                definition = definition.replaceAll("([---])", "<br />$1");
            }
        }

        definition = definition.replaceAll("([---])",
                String.format("<span style='color: %s'>%s</span>", subdefHtmlColor, "$1"));

        html += String.format("<span style='color: %s'>%s</span>", definitionHtmlColor, definition);

        // Format examples
        if (isEpwing && !isEpwingRaw) {
            if (ocrSettingsEpwingShowExamples) {
                List<Example> exampleList = Example.getBestExamples(entry.Expression, entry.ExampleList,
                        ocrSettingsEpwingMaxExamples);
                String exampleStr = "<br />";

                for (int lineIdx = 0; lineIdx < exampleList.size(); lineIdx++) {
                    exampleStr += this.formatSingleExample(exampleList.get(lineIdx), exJapHtmlColor, exEngHtmlColor,
                            exPrependHtmlColor);

                    if (lineIdx != exampleList.size() - 1) {
                        exampleStr += "<br />";
                    }
                }

                if (ocrSettingsEpwingCompactExamples) {
                    exampleStr = "<br />" + exampleStr.replaceAll("<br />", " ");
                }

                html += exampleStr;
            }
        }

        return html;
    }

    /** Format the string for a single example. */
    private String formatSingleExample(Example example, String japColor, String engColor, String prependColor) {
        String exampleText = example.Text;

        String[] fields = exampleText.split("\t");

        String japText = "";
        String engText = "";

        japText = String.format("<span style='color: %s'>%s</span>", japColor,
                UtilsFormatting.addPunctuationToJapText(fields[0].trim()));

        if (fields.length == 2) {
            engText = String.format("<span style='color: %s'>%s</span>", engColor,
                    UtilsFormatting.addPunctuationToEngText(fields[1].trim()));
        }

        exampleText = String.format("%s %s", japText, engText).trim();

        // Get rid of any left over tabs
        exampleText = exampleText.replaceAll("\t", "");

        exampleText = String.format("<span style='color: %s'></span>%s", prependColor, exampleText);

        return exampleText;
    }

    /** Is the capture box currently being adjusted? */
    private boolean isCaptureBoxBeingAdjusted() {
        return ((Calendar.getInstance().getTimeInMillis() - this.lastAdjustment.getTimeInMillis()) < 200);
    }

    /** Returns true is the provided capture boxes have equal dimensions. */
    public boolean isCaptureBoxEqual(Rect captureBoxOld, Rect captureBoxNew) {
        if ((captureBoxOld == null) || (captureBoxNew == null)) {
            return false;
        }

        return ((captureBoxOld.bottom == captureBoxNew.bottom) && (captureBoxOld.right == captureBoxNew.right)
                && (captureBoxOld.top == captureBoxNew.top) && (captureBoxOld.left == captureBoxNew.left));
    }

    /** Task run by the capture timer that is responsible for periodically performing a capture and OCR. */
    class UpdateCaptureTask extends TimerTask {
        /** Number of milliseconds to wait in between captures. */
        final private long CAPTURE_UPDATE_RATE = 20;

        final Handler redrawOcrViewHandler = new Handler();
        final Handler updateDicViewHandler = new Handler();

        /** Runnable that will redraw ocrView */
        final Runnable redrawOcrViewRunnable = new Runnable() {
            public void run() {
                ocrView.invalidate();
            }
        };

        /** Runnable that will update dicView */
        final Runnable updateDicViewRunnable = new Runnable() {
            public void run() {
                try {
                    updateDicViewText();
                } catch (Exception e) {
                    Log.e(LOG_TAG, "Exception in updateDicViewRunnable()! " + e);
                }
            }
        };

        // Capture and OCR, then send the results to the appropriate views.
        public void run() {
            try {
                if (captureBox != null) {
                    // If user is still adjusting capture box
                    if (isCaptureBoxBeingAdjusted()) {
                        return;
                    }

                    if ((Calendar.getInstance().getTimeInMillis()
                            - lastCaptureTime.getTimeInMillis()) >= CAPTURE_UPDATE_RATE) {
                        Rect curCaptureBox = new Rect(captureBox);

                        // Make sure box is big enough to matter
                        if ((curCaptureBox.width() <= 2) || (curCaptureBox.height() <= 2)) {
                            return;
                        }

                        if (forceUpdate || !isCaptureBoxEqual(lastCaptureBox, curCaptureBox)) {
                            forceUpdate = false;

                            // Capture area under the capture box

                            boolean captureResult = captureScreen(curCaptureBox);

                            if (!captureResult) {
                                return;
                            }

                            // Did the user adjust the capture box while capture took place?
                            if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(captureBox, curCaptureBox)) {
                                return;
                            }

                            // OCR the captured area
                            boolean ocrResult = ocrCapture(curCaptureBox);

                            if (!ocrResult) {
                                return;
                            }

                            // Did the user adjust the capture box while OCR took place?
                            if (isCaptureBoxBeingAdjusted() || !isCaptureBoxEqual(captureBox, curCaptureBox)) {
                                return;
                            }

                            // Offset the bounding boxes
                            for (Rect bbox : boundingBoxes) {
                                bbox.offset(clipOffset.x, clipOffset.y);
                            }

                            // Pass capture and OCR info to the OCR view
                            ocrView.setBoundingBoxes(boundingBoxes, getIdealPreProcessingScaleFactor());
                            ocrView.setLastCapture(lastCapture);

                            // Update the dictionary view 
                            updateDicViewHandler.post(updateDicViewRunnable);

                            // Redraw the OCR view 
                            redrawOcrViewHandler.post(redrawOcrViewRunnable);

                            // Save the capture box for comparison later
                            lastCaptureBox = new Rect(curCaptureBox);
                        }

                        // Save the time for comparison later
                        lastCaptureTime.setTimeInMillis(Calendar.getInstance().getTimeInMillis());
                    }

                }
            } catch (Exception e) {
                Log.e(LOG_TAG, "Exception in UpdateCaptureTask.run()! " + e);
            }
        }
    }

}