Merge "Reset forgotten members (D3)"
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index fd7aac4..f9d51ff 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -269,8 +269,7 @@
     <string name="research_feedback_dialog_title" translatable="false">Send feedback</string>
     <!-- Text for checkbox option to include user data in feedback for research purposes [CHAR LIMIT=50] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
-    <!-- TODO: handle multilingual plurals -->
-    <string name="research_feedback_include_history_label" translatable="false">Include last <xliff:g id="word">%d</xliff:g> words entered</string>
+    <string name="research_feedback_include_history_label" translatable="false">Include session history</string>
     <!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_hint" translatable="false">Enter your feedback here.</string>
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 572f290..b7ca60f 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -65,10 +65,6 @@
         return mSuggestedWordInfoList.get(pos).mWord;
     }
 
-    public SuggestedWordInfo getWordInfo(int pos) {
-        return mSuggestedWordInfoList.get(pos);
-    }
-
     public SuggestedWordInfo getInfo(int pos) {
         return mSuggestedWordInfoList.get(pos);
     }
@@ -113,7 +109,7 @@
         alreadySeen.add(typedWord.toString());
         final int previousSize = previousSuggestions.size();
         for (int pos = 1; pos < previousSize; pos++) {
-            final SuggestedWordInfo prevWordInfo = previousSuggestions.getWordInfo(pos);
+            final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(pos);
             final String prevWord = prevWordInfo.mWord;
             // Filter out duplicate suggestion.
             if (!alreadySeen.contains(prevWord)) {
diff --git a/java/src/com/android/inputmethod/research/JsonUtils.java b/java/src/com/android/inputmethod/research/JsonUtils.java
index 1dfd01c..ceba08d 100644
--- a/java/src/com/android/inputmethod/research/JsonUtils.java
+++ b/java/src/com/android/inputmethod/research/JsonUtils.java
@@ -98,7 +98,7 @@
         jsonWriter.beginArray();
         final int size = words.size();
         for (int j = 0; j < size; j++) {
-            final SuggestedWordInfo wordInfo = words.getWordInfo(j);
+            final SuggestedWordInfo wordInfo = words.getInfo(j);
             jsonWriter.value(wordInfo.toString());
         }
         jsonWriter.endArray();
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index cfba289..70bbf9d 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -60,6 +60,7 @@
     private String mWord;
     private boolean mMayContainDigit;
     private boolean mIsPartOfMegaword;
+    private boolean mContainsCorrection;
 
     public LogUnit() {
         mLogStatementList = new ArrayList<LogStatement>();
@@ -274,6 +275,14 @@
         return mMayContainDigit;
     }
 
+    public void setContainsCorrection() {
+        mContainsCorrection = true;
+    }
+
+    public boolean containsCorrection() {
+        return mContainsCorrection;
+    }
+
     public boolean isEmpty() {
         return mLogStatementList.isEmpty();
     }
@@ -301,6 +310,7 @@
                         true /* isPartOfMegaword */);
                 newLogUnit.mWord = null;
                 newLogUnit.mMayContainDigit = mMayContainDigit;
+                newLogUnit.mContainsCorrection = mContainsCorrection;
 
                 // Purge the logStatements and associated data from this LogUnit.
                 laterLogStatements.clear();
@@ -320,6 +330,7 @@
         mTimeList.addAll(logUnit.mTimeList);
         mWord = null;
         mMayContainDigit = mMayContainDigit || logUnit.mMayContainDigit;
+        mContainsCorrection = mContainsCorrection || logUnit.mContainsCorrection;
         mIsPartOfMegaword = false;
     }
 }
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index f4249a0..a46216c 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -106,7 +106,8 @@
     // Change the default indicator to something very visible.  Currently two red vertical bars on
     // either side of they keyboard.
     private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || IS_LOGGING_EVERYTHING;
-    public static final int FEEDBACK_WORD_BUFFER_SIZE = 5;
+    // FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself.
+    public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1;
 
     // constants related to specific log points
     private static final String WHITESPACE_SEPARATORS = " \t\n\r";
@@ -391,9 +392,7 @@
         }
         if (mFeedbackLogBuffer == null) {
             mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
-            // LogBuffer is one more than FEEDBACK_WORD_BUFFER_SIZE, because it must also hold
-            // the feedback LogUnit itself.
-            mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE + 1);
+            mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
         }
     }
 
@@ -522,8 +521,25 @@
     */
 
     private boolean mInFeedbackDialog = false;
+
+    // The feedback dialog causes stop() to be called for the keyboard connected to the original
+    // window.  This is because the feedback dialog must present its own EditText box that displays
+    // a keyboard.  stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be
+    // cleared, and causes mFeedbackLog, which is ready to collect information in case the user
+    // wants to upload, to be closed.  This is good because we don't need to log information about
+    // what the user is typing in the feedback dialog, but bad because this data must be uploaded.
+    // Here we save the LogBuffer and Log so the feedback dialog can later access their data.
+    private LogBuffer mSavedFeedbackLogBuffer;
+    private ResearchLog mSavedFeedbackLog;
+
     public void presentFeedbackDialog(LatinIME latinIME) {
         mInFeedbackDialog = true;
+        mSavedFeedbackLogBuffer = mFeedbackLogBuffer;
+        mSavedFeedbackLog = mFeedbackLog;
+        // Set the non-saved versions to null so that the stop() caused by switching to the
+        // Feedback dialog will not close them.
+        mFeedbackLogBuffer = null;
+        mFeedbackLog = null;
         latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
     }
 
@@ -589,28 +605,25 @@
     }
 
     private static final LogStatement LOGSTATEMENT_FEEDBACK =
-            new LogStatement("UserTimestamp", false, false, "contents");
+            new LogStatement("UserFeedback", false, false, "contents");
     public void sendFeedback(final String feedbackContents, final boolean includeHistory) {
-        if (mFeedbackLogBuffer == null) {
+        if (mSavedFeedbackLogBuffer == null) {
             return;
         }
-        if (includeHistory) {
-            commitCurrentLogUnit();
-        } else {
-            mFeedbackLogBuffer.clear();
+        if (!includeHistory) {
+            mSavedFeedbackLogBuffer.clear();
         }
         final LogUnit feedbackLogUnit = new LogUnit();
         feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
                 feedbackContents);
         mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
-        publishLogBuffer(mFeedbackLogBuffer, mFeedbackLog, true /* isIncludingPrivateData */);
-        mFeedbackLog.close(new Runnable() {
+        publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
+        mSavedFeedbackLog.close(new Runnable() {
             @Override
             public void run() {
                 uploadNow();
             }
         });
-        mFeedbackLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME);
     }
 
     public void uploadNow() {
@@ -643,13 +656,6 @@
     }
 
     private boolean isAllowedToLog() {
-        if (DEBUG) {
-            Log.d(TAG, "iatl: " +
-                "mipw=" + mIsPasswordView +
-                ", mils=" + mIsLoggingSuspended +
-                ", sil=" + sIsLogging +
-                ", mInFeedbackDialog=" + mInFeedbackDialog);
-        }
         return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
     }
 
@@ -714,6 +720,10 @@
         mCurrentLogUnit.setMayContainDigit();
     }
 
+    private void setCurrentLogUnitContainsCorrection() {
+        mCurrentLogUnit.setContainsCorrection();
+    }
+
     /* package for test */ void commitCurrentLogUnit() {
         if (DEBUG) {
             Log.d(TAG, "commitCurrentLogUnit" + (mCurrentLogUnit.hasWord() ?
@@ -844,7 +854,7 @@
             mCurrentLogUnit.setWord(word);
             final boolean isDictionaryWord = dictionary != null
                     && dictionary.isValidWord(word);
-            mStatistics.recordWordEntered(isDictionaryWord);
+            mStatistics.recordWordEntered(isDictionaryWord, mCurrentLogUnit.containsCorrection());
         }
         final LogUnit newLogUnit = mCurrentLogUnit.splitByTime(maxTime);
         enqueueCommitText(word, isBatchMode);
@@ -852,6 +862,11 @@
         mCurrentLogUnit = newLogUnit;
     }
 
+    /**
+     * Record the time of a MotionEvent.ACTION_DOWN.
+     *
+     * Warning: Not thread safe.  Only call from the main thread.
+     */
     private void setSavedDownEventTime(final long time) {
         mSavedDownEventTime = time;
     }
@@ -1170,6 +1185,7 @@
                 scrubDigitsFromString(replacedWord), index,
                 suggestion == null ? null : scrubbedWord, Constants.SUGGESTION_STRIP_COORDINATE,
                 Constants.SUGGESTION_STRIP_COORDINATE);
+        researchLogger.setCurrentLogUnitContainsCorrection();
         researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, Long.MAX_VALUE, isBatchMode);
         researchLogger.mStatistics.recordManualSuggestion(SystemClock.uptimeMillis());
     }
@@ -1329,6 +1345,9 @@
         researchLogger.enqueueEvent(logUnit != null ? logUnit : researchLogger.mCurrentLogUnit,
                 LOGSTATEMENT_LATINIME_REVERTCOMMIT, committedWord, originallyTypedWord,
                 separatorString);
+        if (logUnit != null) {
+            logUnit.setContainsCorrection();
+        }
         researchLogger.mStatistics.recordRevertCommit(SystemClock.uptimeMillis());
         researchLogger.commitCurrentLogUnitAsWord(originallyTypedWord, Long.MAX_VALUE, isBatchMode);
     }
@@ -1475,20 +1494,21 @@
 
     private boolean isExpectingCommitText = false;
     /**
-     * Log a call to RichInputConnection.commitPartialText
+     * Log a call to (UnknownClass).commitPartialText
      *
      * SystemResponse: The IME is committing part of a word.  This happens if a space is
      * automatically inserted to split a single typed string into two or more words.
      */
     // TODO: This method is currently unused.  Find where it should be called from in the IME and
     // add invocations.
-    private static final LogStatement LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT =
-            new LogStatement("LatinIMECommitPartialText", true, false, "newCursorPosition");
-    public static void latinIME_commitPartialText(final String committedWord,
+    private static final LogStatement LOGSTATEMENT_COMMIT_PARTIAL_TEXT =
+            new LogStatement("CommitPartialText", true, false, "newCursorPosition");
+    public static void commitPartialText(final String committedWord,
             final long lastTimestampOfWordData, final boolean isBatchMode) {
         final ResearchLogger researchLogger = getInstance();
         final String scrubbedWord = scrubDigitsFromString(committedWord);
-        researchLogger.enqueueEvent(LOGSTATEMENT_LATINIME_COMMIT_PARTIAL_TEXT);
+        researchLogger.enqueueEvent(LOGSTATEMENT_COMMIT_PARTIAL_TEXT);
+        researchLogger.mStatistics.recordAutoCorrection(SystemClock.uptimeMillis());
         researchLogger.commitCurrentLogUnitAsWord(scrubbedWord, lastTimestampOfWordData,
                 isBatchMode);
     }
@@ -1729,7 +1749,7 @@
                     "averageTimeDuringRepeatedDelete", "averageTimeAfterDelete",
                     "dictionaryWordCount", "splitWordsCount", "gestureInputCount",
                     "gestureCharsCount", "gesturesDeletedCount", "manualSuggestionsCount",
-                    "revertCommitsCount");
+                    "revertCommitsCount", "correctedWordsCount", "autoCorrectionsCount");
     private static void logStatistics() {
         final ResearchLogger researchLogger = getInstance();
         final Statistics statistics = researchLogger.mStatistics;
@@ -1743,6 +1763,7 @@
                 statistics.mDictionaryWordCount, statistics.mSplitWordsCount,
                 statistics.mGesturesInputCount, statistics.mGesturesCharsCount,
                 statistics.mGesturesDeletedCount, statistics.mManualSuggestionsCount,
-                statistics.mRevertCommitsCount);
+                statistics.mRevertCommitsCount, statistics.mCorrectedWordsCount,
+                statistics.mAutoCorrectionsCount);
     }
 }
diff --git a/java/src/com/android/inputmethod/research/Statistics.java b/java/src/com/android/inputmethod/research/Statistics.java
index a920265..f0cb157 100644
--- a/java/src/com/android/inputmethod/research/Statistics.java
+++ b/java/src/com/android/inputmethod/research/Statistics.java
@@ -25,6 +25,7 @@
     private static final String TAG = Statistics.class.getSimpleName();
     private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
 
+    // TODO: Cleanup comments to only including those giving meaningful information.
     // Number of characters entered during a typing session
     int mCharCount;
     // Number of letter characters entered during a typing session
@@ -41,6 +42,8 @@
     int mDictionaryWordCount;
     // Number of words split and spaces automatically entered.
     int mSplitWordsCount;
+    // Number of words entered during a session.
+    int mCorrectedWordsCount;
     // Number of gestures that were input.
     int mGesturesInputCount;
     // Number of gestures that were deleted.
@@ -49,6 +52,8 @@
     int mGesturesCharsCount;
     // Number of manual suggestions chosen.
     int mManualSuggestionsCount;
+    // Number of times that autocorrection was invoked.
+    int mAutoCorrectionsCount;
     // Number of times a commit was reverted in this session.
     int mRevertCommitsCount;
     // Whether the text field was empty upon editing
@@ -113,10 +118,12 @@
         mWordCount = 0;
         mDictionaryWordCount = 0;
         mSplitWordsCount = 0;
+        mCorrectedWordsCount = 0;
         mGesturesInputCount = 0;
         mGesturesDeletedCount = 0;
         mManualSuggestionsCount = 0;
         mRevertCommitsCount = 0;
+        mAutoCorrectionsCount = 0;
         mIsEmptyUponStarting = true;
         mIsEmptinessStateKnown = false;
         mKeyCounter.reset();
@@ -152,11 +159,15 @@
         }
     }
 
-    public void recordWordEntered(final boolean isDictionaryWord) {
+    public void recordWordEntered(final boolean isDictionaryWord,
+            final boolean containsCorrection) {
         mWordCount++;
         if (isDictionaryWord) {
             mDictionaryWordCount++;
         }
+        if (containsCorrection) {
+            mCorrectedWordsCount++;
+        }
     }
 
     public void recordSplitWords() {
@@ -184,6 +195,11 @@
         recordUserAction(time, false /* isDeletion */);
     }
 
+    public void recordAutoCorrection(final long time) {
+        mAutoCorrectionsCount++;
+        recordUserAction(time, false /* isDeletion */);
+    }
+
     public void recordRevertCommit(final long time) {
         mRevertCommitsCount++;
         recordUserAction(time, true /* isDeletion */);
diff --git a/native/jni/src/additional_proximity_chars.h b/native/jni/src/additional_proximity_chars.h
index 21eb0bf..a88fd6c 100644
--- a/native/jni/src/additional_proximity_chars.h
+++ b/native/jni/src/additional_proximity_chars.h
@@ -45,7 +45,7 @@
     }
 
  public:
-    static int getAdditionalCharsSize(const char *localeStr, const int c) {
+    static int getAdditionalCharsSize(const char *const localeStr, const int c) {
         if (!isEnLocale(localeStr)) {
             return 0;
         }
@@ -65,7 +65,7 @@
         }
     }
 
-    static const int *getAdditionalChars(const char *localeStr, const int c) {
+    static const int *getAdditionalChars(const char *const localeStr, const int c) {
         if (!isEnLocale(localeStr)) {
             return 0;
         }
diff --git a/native/jni/src/proximity_info.cpp b/native/jni/src/proximity_info.cpp
index 9b99554..08646af 100644
--- a/native/jni/src/proximity_info.cpp
+++ b/native/jni/src/proximity_info.cpp
@@ -94,11 +94,6 @@
     delete[] mProximityCharsArray;
 }
 
-inline int ProximityInfo::getStartIndexFromCoordinates(const int x, const int y) const {
-    return ((y / CELL_HEIGHT) * GRID_WIDTH + (x / CELL_WIDTH))
-            * MAX_PROXIMITY_CHARS_SIZE;
-}
-
 bool ProximityInfo::hasSpaceProximity(const int x, const int y) const {
     if (x < 0 || y < 0) {
         if (DEBUG_DICT) {
@@ -109,7 +104,8 @@
         return false;
     }
 
-    const int startIndex = getStartIndexFromCoordinates(x, y);
+    const int startIndex = ProximityInfoUtils::getStartIndexFromCoordinates(
+            MAX_PROXIMITY_CHARS_SIZE, x, y, CELL_HEIGHT, CELL_WIDTH, GRID_WIDTH);
     if (DEBUG_PROXIMITY_INFO) {
         AKLOGI("hasSpaceProximity: index %d, %d, %d", startIndex, x, y);
     }
@@ -147,100 +143,6 @@
     return getSquaredDistanceFloat(centerX, centerY, touchX, touchY) / SQUARE_FLOAT(keyWidth);
 }
 
-int ProximityInfo::squaredDistanceToEdge(const int keyId, const int x, const int y) const {
-    if (keyId < 0) return true; // NOT_A_ID is -1, but return whenever < 0 just in case
-    const int left = mKeyXCoordinates[keyId];
-    const int top = mKeyYCoordinates[keyId];
-    const int right = left + mKeyWidths[keyId];
-    const int bottom = top + mKeyHeights[keyId];
-    const int edgeX = x < left ? left : (x > right ? right : x);
-    const int edgeY = y < top ? top : (y > bottom ? bottom : y);
-    const int dx = x - edgeX;
-    const int dy = y - edgeY;
-    return dx * dx + dy * dy;
-}
-
-void ProximityInfo::calculateNearbyKeyCodes(
-        const int x, const int y, const int primaryKey, int *inputCodes) const {
-    int *proximityCharsArray = mProximityCharsArray;
-    int insertPos = 0;
-    inputCodes[insertPos++] = primaryKey;
-    const int startIndex = getStartIndexFromCoordinates(x, y);
-    if (startIndex >= 0) {
-        for (int i = 0; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
-            const int c = proximityCharsArray[startIndex + i];
-            if (c < KEYCODE_SPACE || c == primaryKey) {
-                continue;
-            }
-            const int keyIndex = getKeyIndexOf(c);
-            const bool onKey = isOnKey(keyIndex, x, y);
-            const int distance = squaredDistanceToEdge(keyIndex, x, y);
-            if (onKey || distance < MOST_COMMON_KEY_WIDTH_SQUARE) {
-                inputCodes[insertPos++] = c;
-                if (insertPos >= MAX_PROXIMITY_CHARS_SIZE) {
-                    if (DEBUG_DICT) {
-                        ASSERT(false);
-                    }
-                    return;
-                }
-            }
-        }
-        const int additionalProximitySize =
-                AdditionalProximityChars::getAdditionalCharsSize(mLocaleStr, primaryKey);
-        if (additionalProximitySize > 0) {
-            inputCodes[insertPos++] = ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE;
-            if (insertPos >= MAX_PROXIMITY_CHARS_SIZE) {
-                if (DEBUG_DICT) {
-                    ASSERT(false);
-                }
-                return;
-            }
-
-            const int *additionalProximityChars =
-                    AdditionalProximityChars::getAdditionalChars(mLocaleStr, primaryKey);
-            for (int j = 0; j < additionalProximitySize; ++j) {
-                const int ac = additionalProximityChars[j];
-                int k = 0;
-                for (; k < insertPos; ++k) {
-                    if (ac == inputCodes[k]) {
-                        break;
-                    }
-                }
-                if (k < insertPos) {
-                    continue;
-                }
-                inputCodes[insertPos++] = ac;
-                if (insertPos >= MAX_PROXIMITY_CHARS_SIZE) {
-                    if (DEBUG_DICT) {
-                        ASSERT(false);
-                    }
-                    return;
-                }
-            }
-        }
-    }
-    // Add a delimiter for the proximity characters
-    for (int i = insertPos; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
-        inputCodes[i] = NOT_A_CODE_POINT;
-    }
-}
-
-int ProximityInfo::getKeyIndexOf(const int c) const {
-    if (KEY_COUNT == 0) {
-        // We do not have the coordinate data
-        return NOT_AN_INDEX;
-    }
-    if (c == NOT_A_CODE_POINT) {
-        return NOT_AN_INDEX;
-    }
-    const int lowerCode = toLowerCase(c);
-    hash_map_compat<int, int>::const_iterator mapPos = mCodeToKeyMap.find(lowerCode);
-    if (mapPos != mCodeToKeyMap.end()) {
-        return mapPos->second;
-    }
-    return NOT_AN_INDEX;
-}
-
 int ProximityInfo::getCodePointOf(const int keyIndex) const {
     if (keyIndex < 0 || keyIndex >= KEY_COUNT) {
         return NOT_A_CODE_POINT;
@@ -269,11 +171,13 @@
 }
 
 int ProximityInfo::getKeyCenterXOfCodePointG(int charCode) const {
-    return getKeyCenterXOfKeyIdG(getKeyIndexOf(charCode));
+    return getKeyCenterXOfKeyIdG(
+            ProximityInfoUtils::getKeyIndexOf(KEY_COUNT, charCode, &mCodeToKeyMap));
 }
 
 int ProximityInfo::getKeyCenterYOfCodePointG(int charCode) const {
-    return getKeyCenterYOfKeyIdG(getKeyIndexOf(charCode));
+    return getKeyCenterYOfKeyIdG(
+            ProximityInfoUtils::getKeyIndexOf(KEY_COUNT, charCode, &mCodeToKeyMap));
 }
 
 int ProximityInfo::getKeyCenterXOfKeyIdG(int keyId) const {
diff --git a/native/jni/src/proximity_info.h b/native/jni/src/proximity_info.h
index d002283..6be42a0 100644
--- a/native/jni/src/proximity_info.h
+++ b/native/jni/src/proximity_info.h
@@ -20,6 +20,7 @@
 #include "defines.h"
 #include "hash_map_compat.h"
 #include "jni.h"
+#include "proximity_info_utils.h"
 
 namespace latinime {
 
@@ -40,7 +41,6 @@
     float getNormalizedSquaredDistanceFromCenterFloatG(
             const int keyId, const int x, const int y) const;
     bool sameAsTyped(const unsigned short *word, int length) const;
-    int getKeyIndexOf(const int c) const;
     int getCodePointOf(const int keyIndex) const;
     bool hasSweetSpotData(const int keyIndex) const {
         // When there are no calibration data for a key,
@@ -109,23 +109,26 @@
     int getKeyCenterYOfKeyIdG(int keyId) const;
     int getKeyKeyDistanceG(int keyId0, int keyId1) const;
 
+    void initializeProximities(const int *const inputCodes, const int *const inputXCoordinates,
+            const int *const inputYCoordinates, const int inputSize, int *allInputCodes) const {
+        ProximityInfoUtils::initializeProximities(inputCodes, inputXCoordinates, inputYCoordinates,
+                inputSize, mKeyXCoordinates, mKeyYCoordinates, mKeyWidths, mKeyHeights,
+                mProximityCharsArray, MAX_PROXIMITY_CHARS_SIZE, CELL_HEIGHT, CELL_WIDTH,
+                GRID_WIDTH, MOST_COMMON_KEY_WIDTH, KEY_COUNT, mLocaleStr, &mCodeToKeyMap,
+                allInputCodes);
+    }
+
+    int getKeyIndexOf(const int c) const {
+        return ProximityInfoUtils::getKeyIndexOf(KEY_COUNT, c, &mCodeToKeyMap);
+    }
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(ProximityInfo);
     static const float NOT_A_DISTANCE_FLOAT;
 
-    int getStartIndexFromCoordinates(const int x, const int y) const;
     void initializeG();
     float calculateNormalizedSquaredDistance(const int keyIndex, const int inputIndex) const;
     bool hasInputCoordinates() const;
-    int squaredDistanceToEdge(const int keyId, const int x, const int y) const;
-    bool isOnKey(const int keyId, const int x, const int y) const {
-        if (keyId < 0) return true; // NOT_A_ID is -1, but return whenever < 0 just in case
-        const int left = mKeyXCoordinates[keyId];
-        const int top = mKeyYCoordinates[keyId];
-        const int right = left + mKeyWidths[keyId] + 1;
-        const int bottom = top + mKeyHeights[keyId];
-        return left < right && top < bottom && x >= left && x < right && y >= top && y < bottom;
-    }
 
     const int MAX_PROXIMITY_CHARS_SIZE;
     const int GRID_WIDTH;
diff --git a/native/jni/src/proximity_info_state.cpp b/native/jni/src/proximity_info_state.cpp
index aa02929..1e1413a 100644
--- a/native/jni/src/proximity_info_state.cpp
+++ b/native/jni/src/proximity_info_state.cpp
@@ -53,33 +53,11 @@
     mGridHeight = proximityInfo->getGridWidth();
     mGridWidth = proximityInfo->getGridHeight();
 
-    memset(mInputCodes, 0, sizeof(mInputCodes));
+    memset(mInputProximities, 0, sizeof(mInputProximities));
 
     if (!isGeometric && pointerId == 0) {
-        // Initialize
-        // - mInputCodes
-        // - mNormalizedSquaredDistances
-        // TODO: Merge
-        for (int i = 0; i < inputSize; ++i) {
-            const int primaryKey = inputCodes[i];
-            const int x = xCoordinates[i];
-            const int y = yCoordinates[i];
-            int *proximities = &mInputCodes[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL];
-            mProximityInfo->calculateNearbyKeyCodes(x, y, primaryKey, proximities);
-        }
-
-        if (DEBUG_PROXIMITY_CHARS) {
-            for (int i = 0; i < inputSize; ++i) {
-                AKLOGI("---");
-                for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL; ++j) {
-                    int icc = mInputCodes[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + j];
-                    int icfjc = inputCodes[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + j];
-                    icc += 0;
-                    icfjc += 0;
-                    AKLOGI("--- (%d)%c,%c", i, icc, icfjc); AKLOGI("--- A<%d>,B<%d>", icc, icfjc);
-                }
-            }
-        }
+        mProximityInfo->initializeProximities(inputCodes, xCoordinates, yCoordinates,
+                inputSize, mInputProximities);
     }
 
     ///////////////////////
diff --git a/native/jni/src/proximity_info_state.h b/native/jni/src/proximity_info_state.h
index d747bae..bc2cf50 100644
--- a/native/jni/src/proximity_info_state.h
+++ b/native/jni/src/proximity_info_state.h
@@ -61,7 +61,7 @@
               mInputIndice(), mLengthCache(), mBeelineSpeedPercentiles(), mDistanceCache_G(),
               mSpeedRates(), mDirections(), mCharProbabilities(), mNearKeysVector(),
               mSearchKeysVector(), mTouchPositionCorrectionEnabled(false), mSampledInputSize(0) {
-        memset(mInputCodes, 0, sizeof(mInputCodes));
+        memset(mInputProximities, 0, sizeof(mInputProximities));
         memset(mNormalizedSquaredDistances, 0, sizeof(mNormalizedSquaredDistances));
         memset(mPrimaryInputWord, 0, sizeof(mPrimaryInputWord));
     }
@@ -117,12 +117,12 @@
         if (length != mSampledInputSize) {
             return false;
         }
-        const int *inputCodes = mInputCodes;
+        const int *inputProximities = mInputProximities;
         while (length--) {
-            if (*inputCodes != *word) {
+            if (*inputProximities != *word) {
                 return false;
             }
-            inputCodes += MAX_PROXIMITY_CHARS_SIZE_INTERNAL;
+            inputProximities += MAX_PROXIMITY_CHARS_SIZE_INTERNAL;
             word++;
         }
         return true;
@@ -229,7 +229,7 @@
     }
 
     inline const int *getProximityCodePointsAt(const int index) const {
-        return mInputCodes + (index * MAX_PROXIMITY_CHARS_SIZE_INTERNAL);
+        return mInputProximities + (index * MAX_PROXIMITY_CHARS_SIZE_INTERNAL);
     }
 
     float updateNearKeysDistances(const int x, const int y,
@@ -289,7 +289,7 @@
     // inputs including the current input point.
     std::vector<NearKeycodesSet> mSearchKeysVector;
     bool mTouchPositionCorrectionEnabled;
-    int mInputCodes[MAX_PROXIMITY_CHARS_SIZE_INTERNAL * MAX_WORD_LENGTH];
+    int mInputProximities[MAX_PROXIMITY_CHARS_SIZE_INTERNAL * MAX_WORD_LENGTH];
     int mNormalizedSquaredDistances[MAX_PROXIMITY_CHARS_SIZE_INTERNAL * MAX_WORD_LENGTH];
     int mSampledInputSize;
     int mPrimaryInputWord[MAX_WORD_LENGTH];
diff --git a/native/jni/src/proximity_info_utils.h b/native/jni/src/proximity_info_utils.h
new file mode 100644
index 0000000..b9348d8
--- /dev/null
+++ b/native/jni/src/proximity_info_utils.h
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.
+ */
+
+#ifndef LATINIME_PROXIMITY_INFO_UTILS_H
+#define LATINIME_PROXIMITY_INFO_UTILS_H
+
+#include "additional_proximity_chars.h"
+#include "char_utils.h"
+#include "defines.h"
+#include "hash_map_compat.h"
+
+namespace latinime {
+class ProximityInfoUtils {
+ public:
+    static int getKeyIndexOf(const int keyCount, const int c,
+            const hash_map_compat<int, int> *const codeToKeyMap) {
+        if (keyCount == 0) {
+            // We do not have the coordinate data
+            return NOT_AN_INDEX;
+        }
+        if (c == NOT_A_CODE_POINT) {
+            return NOT_AN_INDEX;
+        }
+        const int lowerCode = toLowerCase(c);
+        hash_map_compat<int, int>::const_iterator mapPos = codeToKeyMap->find(lowerCode);
+        if (mapPos != codeToKeyMap->end()) {
+            return mapPos->second;
+        }
+        return NOT_AN_INDEX;
+    }
+
+    static void initializeProximities(const int *const inputCodes,
+            const int *const inputXCoordinates, const int *const inputYCoordinates,
+            const int inputSize, const int *const keyXCoordinates,
+            const int *const keyYCoordinates, const int *const keyWidths, const int *keyHeights,
+            const int *const proximityCharsArray, const int maxProximityCharsSize,
+            const int cellHeight, const int cellWidth, const int gridWidth,
+            const int mostCommonKeyWidth, const int keyCount, const char *const localeStr,
+            const hash_map_compat<int, int> *const codeToKeyMap, int *inputProximities) {
+        // Initialize
+        // - mInputCodes
+        // - mNormalizedSquaredDistances
+        // TODO: Merge
+        for (int i = 0; i < inputSize; ++i) {
+            const int primaryKey = inputCodes[i];
+            const int x = inputXCoordinates[i];
+            const int y = inputYCoordinates[i];
+            int *proximities = &inputProximities[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL];
+            calculateProximities(keyXCoordinates, keyYCoordinates, keyWidths, keyHeights,
+                    proximityCharsArray, maxProximityCharsSize, cellHeight, cellWidth, gridWidth,
+                    mostCommonKeyWidth, keyCount, x, y, primaryKey, localeStr, codeToKeyMap,
+                    proximities);
+        }
+
+        if (DEBUG_PROXIMITY_CHARS) {
+            for (int i = 0; i < inputSize; ++i) {
+                AKLOGI("---");
+                for (int j = 0; j < MAX_PROXIMITY_CHARS_SIZE_INTERNAL; ++j) {
+                    int proximityChar =
+                            inputProximities[i * MAX_PROXIMITY_CHARS_SIZE_INTERNAL + j];
+                    proximityChar += 0;
+                    AKLOGI("--- (%d)%c", i, proximityChar);
+                }
+            }
+        }
+    }
+
+    AK_FORCE_INLINE static int getStartIndexFromCoordinates(const int maxProximityCharsSize,
+            const int x, const int y, const int cellHeight, const int cellWidth,
+            const int gridWidth) {
+        return ((y / cellHeight) * gridWidth + (x / cellWidth)) * maxProximityCharsSize;
+    }
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(ProximityInfoUtils);
+
+    static bool isOnKey(const int *const keyXCoordinates, const int *const keyYCoordinates,
+            const int *const keyWidths, const int *keyHeights, const int keyId, const int x,
+            const int y) {
+        if (keyId < 0) return true; // NOT_A_ID is -1, but return whenever < 0 just in case
+        const int left = keyXCoordinates[keyId];
+        const int top = keyYCoordinates[keyId];
+        const int right = left + keyWidths[keyId] + 1;
+        const int bottom = top + keyHeights[keyId];
+        return left < right && top < bottom && x >= left && x < right && y >= top && y < bottom;
+    }
+
+    static void calculateProximities(
+            const int *const keyXCoordinates, const int *const keyYCoordinates,
+            const int *const keyWidths, const int *keyHeights,
+            const int *const proximityCharsArray,
+            const int maxProximityCharsSize, const int cellHeight, const int cellWidth,
+            const int gridWidth, const int mostCommonKeyWidth, const int keyCount,
+            const int x, const int y, const int primaryKey, const char *const localeStr,
+            const hash_map_compat<int, int> *const codeToKeyMap, int *proximities) {
+        const int mostCommonKeyWidthSquare = mostCommonKeyWidth * mostCommonKeyWidth;
+        int insertPos = 0;
+        proximities[insertPos++] = primaryKey;
+        const int startIndex = getStartIndexFromCoordinates(
+                maxProximityCharsSize, x, y, cellHeight, cellWidth, gridWidth);
+        if (startIndex >= 0) {
+            for (int i = 0; i < maxProximityCharsSize; ++i) {
+                const int c = proximityCharsArray[startIndex + i];
+                if (c < KEYCODE_SPACE || c == primaryKey) {
+                    continue;
+                }
+                const int keyIndex = getKeyIndexOf(keyCount, c, codeToKeyMap);
+                const bool onKey = isOnKey(keyXCoordinates, keyYCoordinates, keyWidths, keyHeights,
+                        keyIndex, x, y);
+                const int distance = squaredLengthToEdge(keyXCoordinates, keyYCoordinates,
+                        keyWidths, keyHeights, keyIndex, x, y);
+                if (onKey || distance < mostCommonKeyWidthSquare) {
+                    proximities[insertPos++] = c;
+                    if (insertPos >= maxProximityCharsSize) {
+                        if (DEBUG_DICT) {
+                            ASSERT(false);
+                        }
+                        return;
+                    }
+                }
+            }
+            const int additionalProximitySize =
+                    AdditionalProximityChars::getAdditionalCharsSize(localeStr, primaryKey);
+            if (additionalProximitySize > 0) {
+                proximities[insertPos++] = ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE;
+                if (insertPos >= maxProximityCharsSize) {
+                    if (DEBUG_DICT) {
+                        ASSERT(false);
+                    }
+                    return;
+                }
+
+                const int *additionalProximityChars =
+                        AdditionalProximityChars::getAdditionalChars(localeStr, primaryKey);
+                for (int j = 0; j < additionalProximitySize; ++j) {
+                    const int ac = additionalProximityChars[j];
+                    int k = 0;
+                    for (; k < insertPos; ++k) {
+                        if (ac == proximities[k]) {
+                            break;
+                        }
+                    }
+                    if (k < insertPos) {
+                        continue;
+                    }
+                    proximities[insertPos++] = ac;
+                    if (insertPos >= maxProximityCharsSize) {
+                        if (DEBUG_DICT) {
+                            ASSERT(false);
+                        }
+                        return;
+                    }
+                }
+            }
+        }
+        // Add a delimiter for the proximity characters
+        for (int i = insertPos; i < maxProximityCharsSize; ++i) {
+            proximities[i] = NOT_A_CODE_POINT;
+        }
+    }
+
+    static int squaredLengthToEdge(const int *const keyXCoordinates,
+            const int *const keyYCoordinates, const int *const keyWidths, const int *keyHeights,
+            const int keyId, const int x, const int y) {
+        // NOT_A_ID is -1, but return whenever < 0 just in case
+        if (keyId < 0) return MAX_POINT_TO_KEY_LENGTH;
+        const int left = keyXCoordinates[keyId];
+        const int top = keyYCoordinates[keyId];
+        const int right = left + keyWidths[keyId];
+        const int bottom = top + keyHeights[keyId];
+        const int edgeX = x < left ? left : (x > right ? right : x);
+        const int edgeY = y < top ? top : (y > bottom ? bottom : y);
+        const int dx = x - edgeX;
+        const int dy = y - edgeY;
+        return dx * dx + dy * dy;
+    }
+};
+} // namespace latinime
+#endif // LATINIME_PROXIMITY_INFO_UTILS_H