Merge "Change the timing of reading the previous word."
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index d14066c..6445b61 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1779,9 +1779,9 @@
         mInputUpdater.onStartBatchInput();
         mHandler.cancelUpdateSuggestionStrip();
         mConnection.beginBatchEdit();
-        final SettingsValues settingsValues = mSettings.getCurrent();
+        final SettingsValues currentSettingsValues = mSettings.getCurrent();
         if (mWordComposer.isComposingWord()) {
-            if (settingsValues.mIsInternal) {
+            if (currentSettingsValues.mIsInternal) {
                 if (mWordComposer.isBatchMode()) {
                     LatinImeLoggerUtils.onAutoCorrection(
                             "", mWordComposer.getTypedWord(), " ", mWordComposer);
@@ -1808,12 +1808,14 @@
         }
         final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
         if (Character.isLetterOrDigit(codePointBeforeCursor)
-                || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
+                || currentSettingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
             mSpaceState = SPACE_STATE_PHANTOM;
         }
         mConnection.endBatchEdit();
         mKeyboardSwitcher.updateShiftState();
-        mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+        mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(getActualCapsMode(),
+                // Prev word is 1st word before cursor
+                getNthPreviousWordForSuggestion(currentSettingsValues, 1 /* nthPreviousWord */));
     }
 
     static final class InputUpdater implements Handler.Callback {
@@ -1986,7 +1988,8 @@
                     mConnection.commitText(commitParts[0], 0);
                     mSpaceState = SPACE_STATE_PHANTOM;
                     mKeyboardSwitcher.updateShiftState();
-                    mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+                    mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                            getActualCapsMode(), commitParts[0]);
                     ++mAutoCommitSequenceNumber;
                 }
             }
@@ -2295,7 +2298,11 @@
             mWordComposer.add(primaryCode, keyX, keyY);
             // If it's the first letter, make note of auto-caps state
             if (mWordComposer.size() == 1) {
-                mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+                // We pass 1 to getPreviousWordForSuggestion because we were not composing a word
+                // yet, so the word we want is the 1st word before the cursor.
+                mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                        getActualCapsMode(),
+                        getNthPreviousWordForSuggestion(currentSettings, 1 /* nthPreviousWord */));
             }
             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
         } else {
@@ -2537,12 +2544,18 @@
         }
     }
 
-    private String getPreviousWordForSuggestion(final SettingsValues currentSettings) {
+    /**
+     * Get the nth previous word before the cursor as context for the suggestion process.
+     * @param currentSettings the current settings values.
+     * @param nthPreviousWord reverse index of the word to get (1-indexed)
+     * @return the nth previous word before the cursor.
+     */
+    private String getNthPreviousWordForSuggestion(final SettingsValues currentSettings,
+            final int nthPreviousWord) {
         if (currentSettings.mCurrentLanguageHasSpaces) {
             // If we are typing in a language with spaces we can just look up the previous
             // word from textview.
-            return mConnection.getNthPreviousWord(currentSettings,
-                    mWordComposer.isComposingWord() ? 2 : 1);
+            return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord);
         } else {
             return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
                     : mLastComposedWord.mCommittedWord;
@@ -2562,8 +2575,31 @@
         // should just skip whitespace if any, so 1.
         final SettingsValues currentSettings = mSettings.getCurrent();
         final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
-        final String prevWord = getPreviousWordForSuggestion(currentSettings);
-        suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
+
+        final String previousWord;
+        if (mWordComposer.isComposingWord() || mWordComposer.isBatchMode()) {
+            previousWord = mWordComposer.getPreviousWord();
+        } else {
+            // Not composing: this is for prediction.
+            // TODO: read the previous word earlier for prediction, like we are doing for
+            // normal suggestions.
+            previousWord = getNthPreviousWordForSuggestion(currentSettings, 1 /* nthPreviousWord*/);
+        }
+        if (DEBUG) {
+            // TODO: this is for checking consistency with older versions. Remove this when
+            // we are confident this is stable.
+            // We're checking the previous word in the text field against the memorized previous
+            // word. If we are composing a word we should have the second word before the cursor
+            // memorized, otherwise we should have the first.
+            final String rereadPrevWord = getNthPreviousWordForSuggestion(currentSettings,
+                    mWordComposer.isComposingWord() ? 2 : 1);
+            if (!TextUtils.equals(previousWord, rereadPrevWord)) {
+                throw new RuntimeException("Unexpected previous word: "
+                        + previousWord + " <> " + rereadPrevWord);
+            }
+        }
+        suggest.getSuggestedWords(mWordComposer, mWordComposer.getPreviousWord(),
+                keyboard.getProximityInfo(),
                 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
                 additionalFeaturesOptions, sessionId, sequenceNumber, callback);
     }
@@ -2900,7 +2936,13 @@
                 }
             }
         }
-        mWordComposer.setComposingWord(typedWord, mKeyboardSwitcher.getKeyboard());
+        mWordComposer.setComposingWord(typedWord,
+                getNthPreviousWordForSuggestion(currentSettings,
+                        // We want the previous word for suggestion. If we have chars in the word
+                        // before the cursor, then we want the word before that, hence 2; otherwise,
+                        // we want the word immediately before the cursor, hence 1.
+                        0 == numberOfCharsInWordBeforeCursor ? 1 : 2),
+                mKeyboardSwitcher.getKeyboard());
         mWordComposer.setCursorPositionWithinWord(
                 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
         mConnection.setComposingRegion(
@@ -2978,7 +3020,11 @@
     }
 
     private void restartSuggestionsOnWordBeforeCursor(final String word) {
-        mWordComposer.setComposingWord(word, mKeyboardSwitcher.getKeyboard());
+        mWordComposer.setComposingWord(word,
+                // Previous word is the 2nd word before cursor because we are restarting on the
+                // 1st word before cursor.
+                getNthPreviousWordForSuggestion(mSettings.getCurrent(), 2 /* nthPreviousWord */),
+                mKeyboardSwitcher.getKeyboard());
         final int length = word.length();
         mConnection.deleteSurroundingText(length, 0);
         mConnection.setComposingText(word, 1);
@@ -3044,7 +3090,8 @@
         } else {
             // For languages without spaces, we revert the typed string but the cursor is flush
             // with the typed word, so we need to resume suggestions right away.
-            mWordComposer.setComposingWord(stringToCommit, mKeyboardSwitcher.getKeyboard());
+            mWordComposer.setComposingWord(stringToCommit, previousWord,
+                    mKeyboardSwitcher.getKeyboard());
             mConnection.setComposingText(stringToCommit, 1);
         }
         if (mSettings.isInternal()) {
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 039dadc..2f81d15 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -48,6 +48,10 @@
     // at any given time. However this is not limited in size, while mPrimaryKeyCodes is limited
     // to MAX_WORD_LENGTH code points.
     private final StringBuilder mTypedWord;
+    // The previous word (before the composing word). Used as context for suggestions. May be null
+    // after resetting and before starting a new composing word, or when there is no context like
+    // at the start of text for example.
+    private String mPreviousWord;
     private String mAutoCorrection;
     private boolean mIsResumed;
     private boolean mIsBatchMode;
@@ -85,6 +89,7 @@
         mIsBatchMode = false;
         mCursorPositionWithinWord = 0;
         mRejectedBatchModeSuggestion = null;
+        mPreviousWord = null;
         refreshSize();
     }
 
@@ -101,6 +106,7 @@
         mIsBatchMode = source.mIsBatchMode;
         mCursorPositionWithinWord = source.mCursorPositionWithinWord;
         mRejectedBatchModeSuggestion = source.mRejectedBatchModeSuggestion;
+        mPreviousWord = source.mPreviousWord;
         refreshSize();
     }
 
@@ -118,6 +124,7 @@
         mIsBatchMode = false;
         mCursorPositionWithinWord = 0;
         mRejectedBatchModeSuggestion = null;
+        mPreviousWord = null;
         refreshSize();
     }
 
@@ -284,8 +291,13 @@
     /**
      * Set the currently composing word to the one passed as an argument.
      * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
+     * @param word the char sequence to set as the composing word.
+     * @param previousWord the previous word, to use as context for suggestions. Can be null if
+     *   the context is nil (typically, at start of text).
+     * @param keyboard the keyboard this is typed on, for coordinate info/proximity.
      */
-    public void setComposingWord(final CharSequence word, final Keyboard keyboard) {
+    public void setComposingWord(final CharSequence word, final String previousWord,
+            final Keyboard keyboard) {
         reset();
         final int length = word.length();
         for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
@@ -293,6 +305,7 @@
             addKeyInfo(codePoint, keyboard);
         }
         mIsResumed = true;
+        mPreviousWord = previousWord;
     }
 
     /**
@@ -343,6 +356,10 @@
         return mTypedWord.toString();
     }
 
+    public String getPreviousWord() {
+        return mPreviousWord;
+    }
+
     /**
      * Whether or not the user typed a capital letter as the first letter in the word
      * @return capitalization preference
@@ -388,18 +405,21 @@
     }
 
     /**
-     * Saves the caps mode at the start of composing.
+     * Saves the caps mode and the previous word at the start of composing.
      *
-     * WordComposer needs to know about this for several reasons. The first is, we need to know
-     * after the fact what the reason was, to register the correct form into the user history
-     * dictionary: if the word was automatically capitalized, we should insert it in all-lower
-     * case but if it's a manual pressing of shift, then it should be inserted as is.
+     * WordComposer needs to know about the caps mode for several reasons. The first is, we need
+     * to know after the fact what the reason was, to register the correct form into the user
+     * history dictionary: if the word was automatically capitalized, we should insert it in
+     * all-lower case but if it's a manual pressing of shift, then it should be inserted as is.
      * Also, batch input needs to know about the current caps mode to display correctly
      * capitalized suggestions.
      * @param mode the mode at the time of start
+     * @param previousWord the previous word as context for suggestions. May be null if none.
      */
-    public void setCapitalizedModeAtStartComposingTime(final int mode) {
+    public void setCapitalizedModeAndPreviousWordAtStartComposingTime(final int mode,
+            final String previousWord) {
         mCapitalizedMode = mode;
+        mPreviousWord = previousWord;
     }
 
     /**
@@ -451,6 +471,7 @@
         mCapsCount = 0;
         mDigitsCount = 0;
         mIsBatchMode = false;
+        mPreviousWord = mTypedWord.toString();
         mTypedWord.setLength(0);
         mCodePointSize = 0;
         mTrailingSingleQuotesCount = 0;
@@ -464,7 +485,8 @@
         return lastComposedWord;
     }
 
-    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
+    public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord,
+            final String previousWord) {
         mPrimaryKeyCodes = lastComposedWord.mPrimaryKeyCodes;
         mInputPointers.set(lastComposedWord.mInputPointers);
         mTypedWord.setLength(0);
@@ -475,6 +497,7 @@
         mCursorPositionWithinWord = mCodePointSize;
         mRejectedBatchModeSuggestion = null;
         mIsResumed = true;
+        mPreviousWord = previousWord;
     }
 
     public boolean isBatchMode() {
diff --git a/tests/src/com/android/inputmethod/latin/WordComposerTests.java b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
index 1434c6b..6d103ef 100644
--- a/tests/src/com/android/inputmethod/latin/WordComposerTests.java
+++ b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
@@ -26,8 +26,15 @@
 public class WordComposerTests extends AndroidTestCase {
     public void testMoveCursor() {
         final WordComposer wc = new WordComposer();
+        // BMP is the Basic Multilingual Plane, as defined by Unicode. This includes
+        // most characters for most scripts, including all Roman alphabet languages,
+        // CJK, Arabic, Hebrew. Notable exceptions include some emoji and some
+        // very rare Chinese ideograms. BMP characters can be encoded on 2 bytes
+        // in UTF-16, whereas those outside the BMP need 4 bytes.
+        // http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane
         final String STR_WITHIN_BMP = "abcdef";
-        wc.setComposingWord(STR_WITHIN_BMP, null);
+        final String PREVWORD = "prevword";
+        wc.setComposingWord(STR_WITHIN_BMP, PREVWORD, null);
         assertEquals(wc.size(),
                 STR_WITHIN_BMP.codePointCount(0, STR_WITHIN_BMP.length()));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
@@ -43,13 +50,19 @@
         // Move the cursor to after the 'f'
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
+        // Check the previous word is still there
+        assertEquals(PREVWORD, wc.getPreviousWord());
         // Move the cursor past the end of the word
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(15));
+        // Do what LatinIME does when the cursor is moved outside of the word,
+        // and check the behavior is correct.
+        wc.reset();
+        assertNull(wc.getPreviousWord());
 
         // \uD861\uDED7 is 𨛗, a character outside the BMP
         final String STR_WITH_SUPPLEMENTARY_CHAR = "abcde\uD861\uDED7fgh";
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null, null);
         assertEquals(wc.size(), STR_WITH_SUPPLEMENTARY_CHAR.codePointCount(0,
                         STR_WITH_SUPPLEMENTARY_CHAR.length()));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
@@ -59,34 +72,40 @@
         assertTrue(wc.isCursorFrontOrMiddleOfComposingWord());
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(1));
         assertFalse(wc.isCursorFrontOrMiddleOfComposingWord());
+        assertNull(wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITHIN_BMP, null);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7));
+        assertEquals(STR_WITHIN_BMP, wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITH_SUPPLEMENTARY_CHAR, null);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(7));
+        assertEquals(STR_WITH_SUPPLEMENTARY_CHAR, wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITHIN_BMP, null);
         wc.setCursorPositionWithinWord(3);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-3));
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-1));
+        assertEquals(STR_WITHIN_BMP, wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null, null);
         wc.setCursorPositionWithinWord(3);
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-9));
+        assertNull(wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, STR_WITH_SUPPLEMENTARY_CHAR, null);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(-10));
+        assertEquals(STR_WITH_SUPPLEMENTARY_CHAR, wc.getPreviousWord());
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null, null);
         assertFalse(wc.moveCursorByAndReturnIfInsideComposingWord(-11));
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null, null);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(0));
 
-        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null);
+        wc.setComposingWord(STR_WITH_SUPPLEMENTARY_CHAR, null, null);
         wc.setCursorPositionWithinWord(2);
         assertTrue(wc.moveCursorByAndReturnIfInsideComposingWord(0));
     }