Merge "Change background color for functional keys in LatinIME" into gingerbread
diff --git a/java/src/com/android/inputmethod/latin/CandidateView.java b/java/src/com/android/inputmethod/latin/CandidateView.java
index 7fcc3d5..4995727 100755
--- a/java/src/com/android/inputmethod/latin/CandidateView.java
+++ b/java/src/com/android/inputmethod/latin/CandidateView.java
@@ -107,7 +107,6 @@
                     }
                     break;
             }
-            
         }
     };
 
@@ -333,6 +332,10 @@
         requestLayout();
     }
 
+    public boolean isShowingAddToDictionaryHint() {
+        return mShowingAddToDictionary;
+    }
+
     public void showAddToDictionaryHint(CharSequence word) {
         ArrayList<CharSequence> suggestions = new ArrayList<CharSequence>();
         suggestions.add(word);
diff --git a/java/src/com/android/inputmethod/latin/EditingUtil.java b/java/src/com/android/inputmethod/latin/EditingUtil.java
index be31cb7..781d7fd 100644
--- a/java/src/com/android/inputmethod/latin/EditingUtil.java
+++ b/java/src/com/android/inputmethod/latin/EditingUtil.java
@@ -16,10 +16,13 @@
 
 package com.android.inputmethod.latin;
 
+import android.text.TextUtils;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.InputConnection;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.regex.Pattern;
 
 /**
@@ -31,6 +34,11 @@
      */
     private static final int LOOKBACK_CHARACTER_NUM = 15;
 
+    // Cache Method pointers
+    private static boolean sMethodsInitialized;
+    private static Method sMethodGetSelectedText;
+    private static Method sMethodSetComposingRegion;
+
     private EditingUtil() {};
 
     /**
@@ -65,36 +73,16 @@
         return extracted.startOffset + extracted.selectionStart;
     }
 
-    private static int getSelectionEnd(InputConnection connection) {
-        ExtractedText extracted = connection.getExtractedText(
-            new ExtractedTextRequest(), 0);
-        if (extracted == null) {
-          return -1;
-        }
-        return extracted.startOffset + extracted.selectionEnd;
-    }
-
     /**
      * @param connection connection to the current text field.
      * @param sep characters which may separate words
+     * @param range the range object to store the result into
      * @return the word that surrounds the cursor, including up to one trailing
      *   separator. For example, if the field contains "he|llo world", where |
      *   represents the cursor, then "hello " will be returned.
      */
     public static String getWordAtCursor(
-            InputConnection connection, String separators) {
-        return getWordAtCursor(connection, separators, null);
-    }
-
-    /**
-     * @param connection connection to the current text field.
-     * @param sep characters which may separate words
-     * @return the word that surrounds the cursor, including up to one trailing
-     *   separator. For example, if the field contains "he|llo world", where |
-     *   represents the cursor, then "hello " will be returned.
-     */
-    public static String getWordAtCursor(
-        InputConnection connection, String separators, Range range) {
+            InputConnection connection, String separators, Range range) {
         Range r = getWordRangeAtCursor(connection, separators, range);
         return (r == null) ? null : r.word;
     }
@@ -204,26 +192,146 @@
         }
     }
 
-    /**
-     * Checks if the cursor is touching/inside a word or the selection is for a whole
-     * word and no more and no less.
-     * @param range the Range object that contains the bounds of the word around the cursor
-     * @param start the start of the selection
-     * @param end the end of the selection, which could be the same as the start, if text is not
-     * in selection mode
-     * @return false if the selection is a partial word or straddling multiple words, true if
-     * the selection is a full word or there is no selection.
-     */
-    public static boolean isFullWordOrInside(Range range, int start, int end) {
-        // Is the cursor inside or touching a word?
-        if (start == end) return true;
+    public static class SelectedWord {
+        public int start;
+        public int end;
+        public CharSequence word;
+    }
 
-        // Is it a selection? Then is the start of the selection the start of the word and
-        // the size of the selection the size of the word? Then return true
-        if (start < end
-                && (range.charsBefore == 0 && range.charsAfter == end - start)) {
-            return true;
+    /**
+     * Takes a character sequence with a single character and checks if the character occurs
+     * in a list of word separators or is empty.
+     * @param singleChar A CharSequence with null, zero or one character
+     * @param wordSeparators A String containing the word separators
+     * @return true if the character is at a word boundary, false otherwise
+     */
+    private static boolean isWordBoundary(CharSequence singleChar, String wordSeparators) {
+        return TextUtils.isEmpty(singleChar) || wordSeparators.contains(singleChar);
+    }
+
+    /**
+     * Checks if the cursor is inside a word or the current selection is a whole word.
+     * @param ic the InputConnection for accessing the text field
+     * @param selStart the start position of the selection within the text field
+     * @param selEnd the end position of the selection within the text field. This could be
+     *               the same as selStart, if there's no selection.
+     * @param wordSeparators the word separator characters for the current language
+     * @return an object containing the text and coordinates of the selected/touching word,
+     *         null if the selection/cursor is not marking a whole word.
+     */
+    public static SelectedWord getWordAtCursorOrSelection(final InputConnection ic,
+            int selStart, int selEnd, String wordSeparators) {
+        if (selStart == selEnd) {
+            // There is just a cursor, so get the word at the cursor
+            EditingUtil.Range range = new EditingUtil.Range();
+            CharSequence touching = getWordAtCursor(ic, wordSeparators, range);
+            if (!TextUtils.isEmpty(touching)) {
+                SelectedWord selWord = new SelectedWord();
+                selWord.word = touching;
+                selWord.start = selStart - range.charsBefore;
+                selWord.end = selEnd + range.charsAfter;
+                return selWord;
+            }
+        } else {
+            // Is the previous character empty or a word separator? If not, return null.
+            CharSequence charsBefore = ic.getTextBeforeCursor(1, 0);
+            if (!isWordBoundary(charsBefore, wordSeparators)) {
+                return null;
+            }
+
+            // Is the next character empty or a word separator? If not, return null.
+            CharSequence charsAfter = ic.getTextAfterCursor(1, 0);
+            if (!isWordBoundary(charsAfter, wordSeparators)) {
+                return null;
+            }
+
+            // Extract the selection alone
+            CharSequence touching = getSelectedText(ic, selStart, selEnd);
+            if (TextUtils.isEmpty(touching)) return null;
+            // Is any part of the selection a separator? If so, return null.
+            final int length = touching.length();
+            for (int i = 0; i < length; i++) {
+                if (wordSeparators.contains(touching.subSequence(i, i + 1))) {
+                    return null;
+                }
+            }
+            // Prepare the selected word
+            SelectedWord selWord = new SelectedWord();
+            selWord.start = selStart;
+            selWord.end = selEnd;
+            selWord.word = touching;
+            return selWord;
         }
-        return false;
+        return null;
+    }
+
+    /**
+     * Cache method pointers for performance
+     */
+    private static void initializeMethodsForReflection() {
+        try {
+            // These will either both exist or not, so no need for separate try/catch blocks.
+            // If other methods are added later, use separate try/catch blocks.
+            sMethodGetSelectedText = InputConnection.class.getMethod("getSelectedText", int.class);
+            sMethodSetComposingRegion = InputConnection.class.getMethod("setComposingRegion",
+                    int.class, int.class);
+        } catch (NoSuchMethodException exc) {
+            // Ignore
+        }
+        sMethodsInitialized = true;
+    }
+
+    /**
+     * Returns the selected text between the selStart and selEnd positions.
+     */
+    private static CharSequence getSelectedText(InputConnection ic, int selStart, int selEnd) {
+        // Use reflection, for backward compatibility
+        CharSequence result = null;
+        if (!sMethodsInitialized) {
+            initializeMethodsForReflection();
+        }
+        if (sMethodGetSelectedText != null) {
+            try {
+                result = (CharSequence) sMethodGetSelectedText.invoke(ic, 0);
+                return result;
+            } catch (InvocationTargetException exc) {
+                // Ignore
+            } catch (IllegalArgumentException e) {
+                // Ignore
+            } catch (IllegalAccessException e) {
+                // Ignore
+            }
+        }
+        // Reflection didn't work, try it the poor way, by moving the cursor to the start,
+        // getting the text after the cursor and moving the text back to selected mode.
+        // TODO: Verify that this works properly in conjunction with 
+        // LatinIME#onUpdateSelection
+        ic.setSelection(selStart, selEnd);
+        result = ic.getTextAfterCursor(selEnd - selStart, 0);
+        ic.setSelection(selStart, selEnd);
+        return result;
+    }
+
+    /**
+     * Tries to set the text into composition mode if there is support for it in the framework.
+     */
+    public static void underlineWord(InputConnection ic, SelectedWord word) {
+        // Use reflection, for backward compatibility
+        // If method not found, there's nothing we can do. It still works but just wont underline
+        // the word.
+        if (!sMethodsInitialized) {
+            initializeMethodsForReflection();
+        }
+        if (sMethodSetComposingRegion != null) {
+            try {
+                sMethodSetComposingRegion.invoke(ic, word.start, word.end);
+            } catch (InvocationTargetException exc) {
+                // Ignore
+            } catch (IllegalArgumentException e) {
+                // Ignore
+            } catch (IllegalAccessException e) {
+                // Ignore
+            }
+        }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/KeyDetector.java b/java/src/com/android/inputmethod/latin/KeyDetector.java
new file mode 100644
index 0000000..11d5f86
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/KeyDetector.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2010 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.inputmethodservice.Keyboard;
+import android.inputmethodservice.Keyboard.Key;
+
+import java.util.List;
+
+abstract class KeyDetector {
+    protected Keyboard mKeyboard;
+    protected Key[] mKeys;
+
+    protected boolean mProximityCorrectOn;
+    protected int mProximityThresholdSquare;
+
+    public Key[] setKeyboard(Keyboard keyboard) {
+        if (keyboard == null)
+            throw new NullPointerException();
+        mKeyboard = keyboard;
+        List<Key> keys = mKeyboard.getKeys();
+        Key[] array = keys.toArray(new Key[keys.size()]);
+        mKeys = array;
+        return array;
+    }
+
+    public void setProximityCorrectionEnabled(boolean enabled) {
+        mProximityCorrectOn = enabled;
+    }
+
+    public boolean isProximityCorrectionEnabled() {
+        return mProximityCorrectOn;
+    }
+
+    public void setProximityThreshold(int threshold) {
+        mProximityThresholdSquare = threshold * threshold;
+    }
+
+    abstract public int[] newCodeArray();
+
+    abstract public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys);
+}
\ No newline at end of file
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 9312ce2..76f774c 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -86,7 +86,6 @@
     static final boolean TRACE = false;
     static final boolean VOICE_INSTALLED = true;
     static final boolean ENABLE_VOICE_BUTTON = true;
-    private static final boolean MODIFY_TEXT_FOR_CORRECTION = false;
 
     private static final String PREF_VIBRATE_ON = "vibrate_on";
     private static final String PREF_SOUND_ON = "sound_on";
@@ -767,16 +766,21 @@
         mLastSelectionEnd = newSelEnd;
 
 
-        // Check if we should go in or out of correction mode.
-        if (isPredictionOn() && mJustRevertedSeparator == null
-                && (candidatesStart == candidatesEnd || newSelStart != oldSelStart
-                        || TextEntryState.isCorrecting())
-                && (newSelStart < newSelEnd - 1 || (!mPredicting))
-                && !mVoiceInputHighlighted) {
-            if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) {
-                postUpdateOldSuggestions();
-            } else {
-                abortCorrection(false);
+        // Don't look for corrections if the keyboard is not visible
+        if (mKeyboardSwitcher != null && mKeyboardSwitcher.getInputView() != null
+                && mKeyboardSwitcher.getInputView().isShown()) {
+            // Check if we should go in or out of correction mode.
+            if (isPredictionOn()
+                    && mJustRevertedSeparator == null
+                    && (candidatesStart == candidatesEnd || newSelStart != oldSelStart
+                            || TextEntryState.isCorrecting())
+                    && (newSelStart < newSelEnd - 1 || (!mPredicting))
+                    && !mVoiceInputHighlighted) {
+                if (isCursorTouchingWord() || mLastSelectionStart < mLastSelectionEnd) {
+                    postUpdateOldSuggestions();
+                } else {
+                    abortCorrection(false);
+                }
             }
         }
     }
@@ -818,7 +822,7 @@
         if (mCompletionOn) {
             mCompletions = completions;
             if (completions == null) {
-                setSuggestions(null, false, false, false);
+                clearSuggestions();
                 return;
             }
 
@@ -1253,7 +1257,7 @@
     private void abortCorrection(boolean force) {
         if (force || TextEntryState.isCorrecting()) {
             getCurrentInputConnection().finishComposingText();
-            setSuggestions(null, false, false, false);
+            clearSuggestions();
         }
     }
 
@@ -1266,7 +1270,9 @@
             // Assume input length is 1. This assumption fails for smiley face insertions.
             mVoiceInput.incrementTextModificationInsertCount(1);
         }
-        abortCorrection(false);
+        if (mLastSelectionStart == mLastSelectionEnd && TextEntryState.isCorrecting()) {
+            abortCorrection(false);
+        }
 
         if (isAlphabet(primaryCode) && isPredictionOn() && !isCursorTouchingWord()) {
             if (!mPredicting) {
@@ -1495,7 +1501,7 @@
         }
 
         // Clear N-best suggestions
-        setSuggestions(null, false, false, true);
+        clearSuggestions();
 
         FieldContext context = new FieldContext(
             getCurrentInputConnection(),
@@ -1602,13 +1608,15 @@
 
         mVoiceInputHighlighted = true;
         mWordToSuggestions.putAll(mVoiceResults.alternatives);
+    }
 
+    private void clearSuggestions() {
+        setSuggestions(null, false, false, false);
     }
 
     private void setSuggestions(
             List<CharSequence> suggestions,
             boolean completions,
-
             boolean typedWordValid,
             boolean haveMinimalSuggestion) {
 
@@ -1652,14 +1660,14 @@
     }
 
     private void showSuggestions(WordComposer word) {
-        //long startTime = System.currentTimeMillis(); // TIME MEASUREMENT!
+        // long startTime = System.currentTimeMillis(); // TIME MEASUREMENT!
         // TODO Maybe need better way of retrieving previous word
         CharSequence prevWord = EditingUtil.getPreviousWord(getCurrentInputConnection(),
                 mWordSeparators);
         List<CharSequence> stringList = mSuggest.getSuggestions(
-            mKeyboardSwitcher.getInputView(), word, false, prevWord);
-        //long stopTime = System.currentTimeMillis(); // TIME MEASUREMENT!
-        //Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime));
+                mKeyboardSwitcher.getInputView(), word, false, prevWord);
+        // long stopTime = System.currentTimeMillis(); // TIME MEASUREMENT!
+        // Log.d("LatinIME","Suggest Total Time - " + (stopTime - startTime));
 
         int[] nextLettersFrequencies = mSuggest.getNextLettersFrequencies();
 
@@ -1780,18 +1788,23 @@
             mJustAddedAutoSpace = true;
         }
 
-        // Fool the state watcher so that a subsequent backspace will not do a revert, unless
-        // we just did a correction, in which case we need to stay in
-        // TextEntryState.State.PICKED_SUGGESTION state.
+        final boolean showingAddToDictionaryHint = index == 0 && mCorrectionMode > 0
+                && !mSuggest.isValidWord(suggestion)
+                && !mSuggest.isValidWord(suggestion.toString().toLowerCase());
+
         if (!correcting) {
+            // Fool the state watcher so that a subsequent backspace will not do a revert, unless
+            // we just did a correction, in which case we need to stay in
+            // TextEntryState.State.PICKED_SUGGESTION state.
             TextEntryState.typedCharacter((char) KEYCODE_SPACE, true);
             setNextSuggestions();
-        } else {
+        } else if (!showingAddToDictionaryHint) {
+            // If we're not showing the "Tap again to save hint", then show corrections again.
             // In case the cursor position doesn't change, make sure we show the suggestions again.
+            clearSuggestions();
             postUpdateOldSuggestions();
         }
-        if (index == 0 && mCorrectionMode > 0 && !mSuggest.isValidWord(suggestion)
-                && !mSuggest.isValidWord(suggestion.toString().toLowerCase())) {
+        if (showingAddToDictionaryHint) {
             mCandidateView.showAddToDictionaryHint(suggestion);
         }
         if (ic != null) {
@@ -1841,16 +1854,6 @@
         InputConnection ic = getCurrentInputConnection();
         if (ic != null) {
             rememberReplacedWord(suggestion);
-            // If text is in correction mode and we're not using composing
-            // text to underline, then the word at the cursor position needs
-            // to be removed before committing the correction
-            if (correcting && !MODIFY_TEXT_FOR_CORRECTION) {
-                if (mLastSelectionStart < mLastSelectionEnd) {
-                    ic.setSelection(mLastSelectionStart, mLastSelectionStart);
-                }
-                EditingUtil.deleteWordAtCursor(ic, getWordSeparators());
-            }
-
             ic.commitText(suggestion, 1);
         }
         saveWordInHistory(suggestion);
@@ -1864,96 +1867,108 @@
         updateShiftKeyState(getCurrentInputEditorInfo());
     }
 
+    /**
+     * Tries to apply any voice alternatives for the word if this was a spoken word and
+     * there are voice alternatives.
+     * @param touching The word that the cursor is touching, with position information
+     * @return true if an alternative was found, false otherwise.
+     */
+    private boolean applyVoiceAlternatives(EditingUtil.SelectedWord touching) {
+        // Search for result in spoken word alternatives
+        String selectedWord = touching.word.toString().trim();
+        if (!mWordToSuggestions.containsKey(selectedWord)) {
+            selectedWord = selectedWord.toLowerCase();
+        }
+        if (mWordToSuggestions.containsKey(selectedWord)) {
+            mShowingVoiceSuggestions = true;
+            List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
+            // If the first letter of touching is capitalized, make all the suggestions
+            // start with a capital letter.
+            if (Character.isUpperCase((char) touching.word.charAt(0))) {
+                for (int i = 0; i < suggestions.size(); i++) {
+                    String origSugg = (String) suggestions.get(i);
+                    String capsSugg = origSugg.toUpperCase().charAt(0)
+                            + origSugg.subSequence(1, origSugg.length()).toString();
+                    suggestions.set(i, capsSugg);
+                }
+            }
+            setSuggestions(suggestions, false, true, true);
+            setCandidatesViewShown(true);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Tries to apply any typed alternatives for the word if we have any cached alternatives,
+     * otherwise tries to find new corrections and completions for the word.
+     * @param touching The word that the cursor is touching, with position information
+     * @return true if an alternative was found, false otherwise.
+     */
+    private boolean applyTypedAlternatives(EditingUtil.SelectedWord touching) {
+        // If we didn't find a match, search for result in typed word history
+        WordComposer foundWord = null;
+        WordAlternatives alternatives = null;
+        for (WordAlternatives entry : mWordHistory) {
+            if (TextUtils.equals(entry.getChosenWord(), touching.word)) {
+                if (entry instanceof TypedWordAlternatives) {
+                    foundWord = ((TypedWordAlternatives) entry).word;
+                }
+                alternatives = entry;
+                break;
+            }
+        }
+        // If we didn't find a match, at least suggest completions
+        if (foundWord == null
+                && (mSuggest.isValidWord(touching.word)
+                        || mSuggest.isValidWord(touching.word.toString().toLowerCase()))) {
+            foundWord = new WordComposer();
+            for (int i = 0; i < touching.word.length(); i++) {
+                foundWord.add(touching.word.charAt(i), new int[] {
+                    touching.word.charAt(i)
+                });
+            }
+            foundWord.setCapitalized(Character.isUpperCase(touching.word.charAt(0)));
+        }
+        // Found a match, show suggestions
+        if (foundWord != null || alternatives != null) {
+            if (alternatives == null) {
+                alternatives = new TypedWordAlternatives(touching.word, foundWord);
+            }
+            showCorrections(alternatives);
+            if (foundWord != null) {
+                mWord = new WordComposer(foundWord);
+            } else {
+                mWord.reset();
+            }
+            return true;
+        }
+        return false;
+    }
+
     private void setOldSuggestions() {
-        // TODO: Inefficient to check if touching word and then get the touching word. Do it
-        // in one go.
         mShowingVoiceSuggestions = false;
+        if (mCandidateView != null && mCandidateView.isShowingAddToDictionaryHint()) {
+            return;
+        }
         InputConnection ic = getCurrentInputConnection();
         if (ic == null) return;
-        ic.beginBatchEdit();
-        // If there is a selection, then undo the selection first. Unfortunately this causes
-        // a flicker. TODO: Add getSelectionText() to InputConnection API.
-        if (mLastSelectionStart < mLastSelectionEnd) {
-            ic.setSelection(mLastSelectionStart, mLastSelectionStart);
-        }
-        if (!mPredicting && isCursorTouchingWord()) {
-            EditingUtil.Range range = new EditingUtil.Range();
-            CharSequence touching = EditingUtil.getWordAtCursor(getCurrentInputConnection(),
-                    mWordSeparators, range);
-            // If it's a selection, check if it's an entire word and no more, no less.
-            boolean fullword = EditingUtil.isFullWordOrInside(range, mLastSelectionStart,
-                    mLastSelectionEnd);
-            if (fullword && touching != null && touching.length() > 1) {
-                // Strip out any trailing word separator
-                if (mWordSeparators.indexOf(touching.charAt(touching.length() - 1)) > 0) {
-                    touching = touching.toString().substring(0, touching.length() - 1);
+        if (!mPredicting) {
+            // Extract the selected or touching text
+            EditingUtil.SelectedWord touching = EditingUtil.getWordAtCursorOrSelection(ic,
+                    mLastSelectionStart, mLastSelectionEnd, mWordSeparators);
+
+            if (touching != null && touching.word.length() > 1) {
+                ic.beginBatchEdit();
+
+                if (!applyVoiceAlternatives(touching) && !applyTypedAlternatives(touching)) {
+                    abortCorrection(true);
+                } else {
+                    TextEntryState.selectedForCorrection();
+                    EditingUtil.underlineWord(ic, touching);
                 }
 
-                // Search for result in spoken word alternatives
-                String selectedWord = touching.toString().trim();
-                if (!mWordToSuggestions.containsKey(selectedWord)){
-                    selectedWord = selectedWord.toLowerCase();
-                }
-                if (mWordToSuggestions.containsKey(selectedWord)){
-                    mShowingVoiceSuggestions = true;
-                    underlineWord(touching, range.charsBefore, range.charsAfter);
-                    List<CharSequence> suggestions = mWordToSuggestions.get(selectedWord);
-                    // If the first letter of touching is capitalized, make all the suggestions
-                    // start with a capital letter.
-                    if (Character.isUpperCase((char) touching.charAt(0))) {
-                        for (int i=0; i< suggestions.size(); i++) {
-                            String origSugg = (String) suggestions.get(i);
-                            String capsSugg = origSugg.toUpperCase().charAt(0)
-                                + origSugg.subSequence(1, origSugg.length()).toString();
-                            suggestions.set(i,capsSugg);
-                        }
-                    }
-                    setSuggestions(suggestions, false, true, true);
-                    setCandidatesViewShown(true);
-                    TextEntryState.selectedForCorrection();
-                    ic.endBatchEdit();
-                    return;
-                }
-
-                // If we didn't find a match, search for result in typed word history
-                WordComposer foundWord = null;
-                WordAlternatives alternatives = null;
-                for (WordAlternatives entry : mWordHistory) {
-                    if (TextUtils.equals(entry.getChosenWord(), touching)) {
-                        if (entry instanceof TypedWordAlternatives) {
-                            foundWord = ((TypedWordAlternatives)entry).word;
-                        }
-                        alternatives = entry;
-                        break;
-                    }
-                }
-                // If we didn't find a match, at least suggest completions
-                if (foundWord == null && mSuggest.isValidWord(touching)) {
-                    foundWord = new WordComposer();
-                    for (int i = 0; i < touching.length(); i++) {
-                        foundWord.add(touching.charAt(i), new int[] { touching.charAt(i) });
-                    }
-                }
-                // Found a match, show suggestions
-                if (foundWord != null || alternatives != null) {
-                    underlineWord(touching, range.charsBefore, range.charsAfter);
-                    TextEntryState.selectedForCorrection();
-                    if (alternatives == null) alternatives = new TypedWordAlternatives(touching,
-                            foundWord);
-                    showCorrections(alternatives);
-                    if (foundWord != null) {
-                        mWord = new WordComposer(foundWord);
-                    } else {
-                        mWord.reset();
-                    }
-                    // Revert the selection
-                    if (mLastSelectionStart < mLastSelectionEnd) {
-                        ic.setSelection(mLastSelectionStart, mLastSelectionEnd);
-                    }
-                    ic.endBatchEdit();
-                    return;
-                }
-                abortCorrection(true);
+                ic.endBatchEdit();
             } else {
                 abortCorrection(true);
                 setNextSuggestions();
@@ -1961,28 +1976,12 @@
         } else {
             abortCorrection(true);
         }
-        // Revert the selection
-        if (mLastSelectionStart < mLastSelectionEnd) {
-            ic.setSelection(mLastSelectionStart, mLastSelectionEnd);
-        }
-        ic.endBatchEdit();
     }
 
     private void setNextSuggestions() {
         setSuggestions(mSuggestPuncList, false, false, false);
     }
 
-    private void underlineWord(CharSequence word, int left, int right) {
-        InputConnection ic = getCurrentInputConnection();
-        if (ic == null) return;
-        if (MODIFY_TEXT_FOR_CORRECTION) {
-            ic.finishComposingText();
-            ic.deleteSurroundingText(left, right);
-            ic.setComposingText(word, 1);
-        }
-        ic.setSelection(mLastSelectionStart, mLastSelectionStart);
-    }
-
     private void addToDictionaries(CharSequence suggestion, int frequencyDelta) {
         checkAddToDictionary(suggestion, frequencyDelta, false);
     }
diff --git a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
index d1a5cd8..4daf651 100644
--- a/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
+++ b/java/src/com/android/inputmethod/latin/LatinKeyboardBaseView.java
@@ -45,7 +45,6 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -202,7 +201,7 @@
     private final ArrayList<PointerTracker> mPointerTrackers = new ArrayList<PointerTracker>();
     private final float mDebounceHysteresis;
 
-    private final ProximityKeyDetector mProximityKeyDetector = new ProximityKeyDetector();
+    protected KeyDetector mKeyDetector = new ProximityKeyDetector();
 
     // Swipe gesture detector
     private final GestureDetector mGestureDetector;
@@ -473,8 +472,7 @@
     public void setOnKeyboardActionListener(OnKeyboardActionListener listener) {
         mKeyboardActionListener = listener;
         for (PointerTracker tracker : mPointerTrackers) {
-            if (tracker != null)
-                tracker.setOnKeyboardActionListener(listener);
+            tracker.setOnKeyboardActionListener(listener);
         }
     }
 
@@ -501,13 +499,10 @@
         mHandler.cancelKeyTimers();
         mHandler.cancelPopupPreview();
         mKeyboard = keyboard;
-        LatinImeLogger.onSetKeyboard(mKeyboard);
-        List<Key> keys = mKeyboard.getKeys();
-        mKeys = keys.toArray(new Key[keys.size()]);
-        mProximityKeyDetector.setKeyboard(keyboard, mKeys);
+        LatinImeLogger.onSetKeyboard(keyboard);
+        mKeys = mKeyDetector.setKeyboard(keyboard);
         for (PointerTracker tracker : mPointerTrackers) {
-            if (tracker != null)
-                tracker.setKeyboard(mKeys, mDebounceHysteresis);
+            tracker.setKeyboard(mKeys, mDebounceHysteresis);
         }
         requestLayout();
         // Hint to reallocate the buffer if the size changed
@@ -599,14 +594,14 @@
      * @param enabled whether or not the proximity correction is enabled
      */
     public void setProximityCorrectionEnabled(boolean enabled) {
-        mProximityKeyDetector.setProximityCorrectionEnabled(enabled);
+        mKeyDetector.setProximityCorrectionEnabled(enabled);
     }
 
     /**
      * Returns true if proximity correction is enabled.
      */
     public boolean isProximityCorrectionEnabled() {
-        return mProximityKeyDetector.isProximityCorrectionEnabled();
+        return mKeyDetector.isProximityCorrectionEnabled();
     }
 
     /**
@@ -658,7 +653,7 @@
             dimensionSum += Math.min(key.width, key.height) + key.gap;
         }
         if (dimensionSum < 0 || length == 0) return;
-        mProximityKeyDetector.setProximityThreshold((int) (dimensionSum * 1.4f / length));
+        mKeyDetector.setProximityThreshold((int) (dimensionSum * 1.4f / length));
     }
 
     @Override
@@ -779,7 +774,6 @@
         if (DEBUG) {
             if (mShowTouchPoints) {
                 for (PointerTracker tracker : mPointerTrackers) {
-                    if (tracker == null) continue;
                     int startX = tracker.getStartX();
                     int startY = tracker.getStartY();
                     int lastX = tracker.getLastX();
@@ -1052,7 +1046,7 @@
         // Create pointer trackers until we can get 'id+1'-th tracker, if needed.
         for (int i = pointers.size(); i <= id; i++) {
             final PointerTracker tracker =
-                new PointerTracker(mHandler, mProximityKeyDetector, this);
+                new PointerTracker(i, mHandler, mKeyDetector, this);
             if (keys != null)
                 tracker.setKeyboard(keys, mDebounceHysteresis);
             if (listener != null)
diff --git a/java/src/com/android/inputmethod/latin/PointerTracker.java b/java/src/com/android/inputmethod/latin/PointerTracker.java
index 3c67ebe..c8976a3 100644
--- a/java/src/com/android/inputmethod/latin/PointerTracker.java
+++ b/java/src/com/android/inputmethod/latin/PointerTracker.java
@@ -32,6 +32,8 @@
         public boolean isMiniKeyboardOnScreen();
     }
 
+    public final int mPointerId;
+
     // Timing constants
     private static final int REPEAT_START_DELAY = 400;
     /* package */  static final int REPEAT_INTERVAL = 50; // ~20 keys per second
@@ -45,7 +47,7 @@
 
     private final UIProxy mProxy;
     private final UIHandler mHandler;
-    private final ProximityKeyDetector mKeyDetector;
+    private final KeyDetector mKeyDetector;
     private OnKeyboardActionListener mListener;
 
     private Key[] mKeys;
@@ -77,9 +79,10 @@
     // pressed key
     private int mPreviousKey = NOT_A_KEY;
 
-    public PointerTracker(UIHandler handler, ProximityKeyDetector keyDetector, UIProxy proxy) {
+    public PointerTracker(int id, UIHandler handler, KeyDetector keyDetector, UIProxy proxy) {
         if (proxy == null || handler == null || keyDetector == null)
             throw new NullPointerException();
+        mPointerId = id;
         mProxy = proxy;
         mHandler = handler;
         mKeyDetector = keyDetector;
@@ -97,21 +100,25 @@
         mKeyDebounceThresholdSquared = (int)(hysteresisPixel * hysteresisPixel);
     }
 
+    private boolean isValidKeyIndex(int keyIndex) {
+        return keyIndex >= 0 && keyIndex < mKeys.length;
+    }
+
     public Key getKey(int keyIndex) {
-        return (keyIndex >= 0 && keyIndex < mKeys.length) ? mKeys[keyIndex] : null;
+        return isValidKeyIndex(keyIndex) ? mKeys[keyIndex] : null;
     }
 
     public void updateKey(int keyIndex) {
         int oldKeyIndex = mPreviousKey;
         mPreviousKey = keyIndex;
         if (keyIndex != oldKeyIndex) {
-            if (oldKeyIndex != NOT_A_KEY && oldKeyIndex < mKeys.length) {
+            if (isValidKeyIndex(oldKeyIndex)) {
                 // if new key index is not a key, old key was just released inside of the key.
                 final boolean inside = (keyIndex == NOT_A_KEY);
                 mKeys[oldKeyIndex].onReleased(inside);
                 mProxy.invalidateKey(mKeys[oldKeyIndex]);
             }
-            if (keyIndex != NOT_A_KEY && keyIndex < mKeys.length) {
+            if (isValidKeyIndex(keyIndex)) {
                 mKeys[keyIndex].onPressed();
                 mProxy.invalidateKey(mKeys[keyIndex]);
             }
@@ -127,14 +134,14 @@
         startTimeDebouncing(eventTime);
         checkMultiTap(eventTime, keyIndex);
         if (mListener != null) {
-            int primaryCode = (keyIndex != NOT_A_KEY) ? mKeys[keyIndex].codes[0] : 0;
+            int primaryCode = isValidKeyIndex(keyIndex) ? mKeys[keyIndex].codes[0] : 0;
             mListener.onPress(primaryCode);
         }
-        if (keyIndex >= 0 && mKeys[keyIndex].repeatable) {
-            repeatKey(keyIndex);
-            mHandler.startKeyRepeatTimer(REPEAT_START_DELAY, keyIndex, this);
-        }
-        if (keyIndex != NOT_A_KEY) {
+        if (isValidKeyIndex(keyIndex)) {
+            if (mKeys[keyIndex].repeatable) {
+                repeatKey(keyIndex);
+                mHandler.startKeyRepeatTimer(REPEAT_START_DELAY, keyIndex, this);
+            }
             mHandler.startLongPressTimer(keyIndex, LONGPRESS_TIMEOUT);
         }
         showKeyPreviewAndUpdateKey(keyIndex);
@@ -143,7 +150,7 @@
 
     public void onMoveEvent(int touchX, int touchY, long eventTime) {
         int keyIndex = mKeyDetector.getKeyIndexAndNearbyCodes(touchX, touchY, null);
-        if (keyIndex != NOT_A_KEY) {
+        if (isValidKeyIndex(keyIndex)) {
             if (mCurrentKey == NOT_A_KEY) {
                 updateTimeDebouncing(eventTime);
                 mCurrentKey = keyIndex;
@@ -192,7 +199,7 @@
         if (!wasInKeyRepeat && !mProxy.isMiniKeyboardOnScreen()) {
             detectAndSendKey(mCurrentKey, touchX, touchY, eventTime);
         }
-        if (keyIndex != NOT_A_KEY && keyIndex < mKeys.length)
+        if (isValidKeyIndex(keyIndex))
             mProxy.invalidateKey(mKeys[keyIndex]);
     }
 
@@ -202,15 +209,17 @@
         mProxy.dismissPopupKeyboard();
         showKeyPreviewAndUpdateKey(NOT_A_KEY);
         int keyIndex = mCurrentKey;
-        if (keyIndex != NOT_A_KEY && keyIndex < mKeys.length)
+        if (isValidKeyIndex(keyIndex))
            mProxy.invalidateKey(mKeys[keyIndex]);
     }
 
     public void repeatKey(int keyIndex) {
-        Key key = mKeys[keyIndex];
-        // While key is repeating, because there is no need to handle multi-tap key, we can pass
-        // -1 as eventTime argument.
-        detectAndSendKey(keyIndex, key.x, key.y, -1);
+        Key key = getKey(keyIndex);
+        if (key != null) {
+            // While key is repeating, because there is no need to handle multi-tap key, we can
+            // pass -1 as eventTime argument.
+            detectAndSendKey(keyIndex, key.x, key.y, -1);
+        }
     }
 
     // These package scope methods are only for debugging purpose.
@@ -250,7 +259,7 @@
             throw new IllegalStateException("keyboard and/or hysteresis not set");
         if (newKey == curKey) {
             return true;
-        } else if (curKey >= 0 && curKey < mKeys.length) {
+        } else if (isValidKeyIndex(curKey)) {
             return getSquareDistanceToKeyEdge(x, y, mKeys[curKey])
                     < mKeyDebounceThresholdSquared;
         } else {
@@ -300,7 +309,7 @@
     }
 
     private void detectAndSendKey(int index, int x, int y, long eventTime) {
-        if (index != NOT_A_KEY && index < mKeys.length) {
+        if (isValidKeyIndex(index)) {
             final Key key = mKeys[index];
             OnKeyboardActionListener listener = mListener;
             if (key.text != null) {
@@ -363,11 +372,15 @@
     }
 
     private void checkMultiTap(long eventTime, int keyIndex) {
-        if (keyIndex == NOT_A_KEY) return;
-        Key key = mKeys[keyIndex];
+        Key key = getKey(keyIndex);
+        if (key == null)
+            return;
+
+        final boolean isMultiTap =
+                (eventTime < mLastTapTime + MULTITAP_INTERVAL && keyIndex == mLastSentIndex);
         if (key.codes.length > 1) {
             mInMultiTap = true;
-            if (eventTime < mLastTapTime + MULTITAP_INTERVAL && keyIndex == mLastSentIndex) {
+            if (isMultiTap) {
                 mTapCount = (mTapCount + 1) % key.codes.length;
                 return;
             } else {
@@ -375,7 +388,7 @@
                 return;
             }
         }
-        if (eventTime > mLastTapTime + MULTITAP_INTERVAL || keyIndex != mLastSentIndex) {
+        if (!isMultiTap) {
             resetMultiTap();
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java b/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java
index eae2d7f..6ee0055 100644
--- a/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java
+++ b/java/src/com/android/inputmethod/latin/ProximityKeyDetector.java
@@ -16,48 +16,24 @@
 
 package com.android.inputmethod.latin;
 
-import android.inputmethodservice.Keyboard;
 import android.inputmethodservice.Keyboard.Key;
 
 import java.util.Arrays;
 
-class ProximityKeyDetector {
+class ProximityKeyDetector extends KeyDetector {
     private static final int MAX_NEARBY_KEYS = 12;
 
-    private Keyboard mKeyboard;
-    private Key[] mKeys;
-
-    private boolean mProximityCorrectOn;
-    private int mProximityThresholdSquare;
-
     // working area
     private int[] mDistances = new int[MAX_NEARBY_KEYS];
 
-    public void setKeyboard(Keyboard keyboard, Key[] keys) {
-        if (keyboard == null || keys == null)
-            throw new NullPointerException();
-        mKeyboard = keyboard;
-        mKeys = keys;
-    }
-
-    public void setProximityCorrectionEnabled(boolean enabled) {
-        mProximityCorrectOn = enabled;
-    }
-
-    public boolean isProximityCorrectionEnabled() {
-        return mProximityCorrectOn;
-    }
-
-    public void setProximityThreshold(int threshold) {
-        mProximityThresholdSquare = threshold * threshold;
-    }
-
+    @Override
     public int[] newCodeArray() {
         int[] codes = new int[MAX_NEARBY_KEYS];
         Arrays.fill(codes, LatinKeyboardBaseView.NOT_A_KEY);
         return codes;
     }
 
+    @Override
     public int getKeyIndexAndNearbyCodes(int x, int y, int[] allKeys) {
         final Key[] keys = mKeys;
         if (keys == null)