Add a sequence number to SuggestedWords.

This allows testing for suggestion freshness in an asynchronous
suggestions world.

In-advance cherrypick of Ic76cd17568598d8534aec81e037f9e37f52eb6b4
because there's a merge conflict.

Bug: 11301597
Change-Id: I4aec765a975298fcac30a48dede73d2622224fe5
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index e8ebf88..2ddef3c 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1836,12 +1836,15 @@
 
         @Override
         public boolean handleMessage(final Message msg) {
+            // TODO: straighten message passing - we don't need two kinds of messages calling
+            // each other.
             switch (msg.what) {
                 case MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP:
-                    updateBatchInput((InputPointers)msg.obj);
+                    updateBatchInput((InputPointers)msg.obj, msg.arg2 /* sequenceNumber */);
                     break;
                 case MSG_GET_SUGGESTED_WORDS:
-                    mLatinIme.getSuggestedWords(msg.arg1, (OnGetSuggestedWordsCallback) msg.obj);
+                    mLatinIme.getSuggestedWords(msg.arg1 /* sessionId */,
+                            msg.arg2 /* sequenceNumber */, (OnGetSuggestedWordsCallback) msg.obj);
                     break;
             }
             return true;
@@ -1858,14 +1861,15 @@
         }
 
         // Run in the Handler thread.
-        private void updateBatchInput(final InputPointers batchPointers) {
+        private void updateBatchInput(final InputPointers batchPointers, final int sequenceNumber) {
             synchronized (mLock) {
                 if (!mInBatchInput) {
                     // Batch input has ended or canceled while the message was being delivered.
                     return;
                 }
 
-                getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
+                getSuggestedWordsGestureLocked(batchPointers, sequenceNumber,
+                        new OnGetSuggestedWordsCallback() {
                     @Override
                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
                         mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
@@ -1876,13 +1880,13 @@
         }
 
         // Run in the UI thread.
-        public void onUpdateBatchInput(final InputPointers batchPointers) {
+        public void onUpdateBatchInput(final InputPointers batchPointers,
+                final int sequenceNumber) {
             if (mHandler.hasMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP)) {
                 return;
             }
-            mHandler.obtainMessage(
-                    MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, batchPointers)
-                    .sendToTarget();
+            mHandler.obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP, 0 /* arg1 */,
+                    sequenceNumber /* arg2 */, batchPointers /* obj */).sendToTarget();
         }
 
         public void onCancelBatchInput() {
@@ -1896,7 +1900,8 @@
         // Run in the UI thread.
         public void onEndBatchInput(final InputPointers batchPointers) {
             synchronized(mLock) {
-                getSuggestedWordsGestureLocked(batchPointers, new OnGetSuggestedWordsCallback() {
+                getSuggestedWordsGestureLocked(batchPointers, SuggestedWords.NOT_A_SEQUENCE_NUMBER,
+                        new OnGetSuggestedWordsCallback() {
                     @Override
                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
                         mInBatchInput = false;
@@ -1911,10 +1916,10 @@
         // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
         // be synchronized.
         private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
-                final OnGetSuggestedWordsCallback callback) {
+                final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
             mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
             mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
-                    new OnGetSuggestedWordsCallback() {
+                    sequenceNumber, new OnGetSuggestedWordsCallback() {
                 @Override
                 public void onGetSuggestedWords(SuggestedWords suggestedWords) {
                     final int suggestionCount = suggestedWords.size();
@@ -1929,9 +1934,10 @@
             });
         }
 
-        public void getSuggestedWords(final int sessionId,
+        public void getSuggestedWords(final int sessionId, final int sequenceNumber,
                 final OnGetSuggestedWordsCallback callback) {
-            mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, 0, callback).sendToTarget();
+            mHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, sessionId, sequenceNumber, callback)
+                    .sendToTarget();
         }
 
         private void onDestroy() {
@@ -1952,11 +1958,28 @@
         }
     }
 
+    /* The sequence number member is only used in onUpdateBatchInput. It is increased each time
+     * auto-commit happens. The reason we need this is, when auto-commit happens we trim the
+     * input pointers that are held in a singleton, and to know how much to trim we rely on the
+     * results of the suggestion process that is held in mSuggestedWords.
+     * However, the suggestion process is asynchronous, and sometimes we may enter the
+     * onUpdateBatchInput method twice without having recomputed suggestions yet, or having
+     * received new suggestions generated from not-yet-trimmed input pointers. In this case, the
+     * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we
+     * remove an unrelated number of pointers (possibly even more than are left in the input
+     * pointers, leading to a crash).
+     * To avoid that, we increase the sequence number each time we auto-commit and trim the
+     * input pointers, and we do not use any suggested words that have been generated with an
+     * earlier sequence number.
+     */
+    private int mAutoCommitSequenceNumber = 1;
     @Override
     public void onUpdateBatchInput(final InputPointers batchPointers) {
         if (mSettings.getCurrent().mPhraseGestureEnabled) {
             final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
-            if (null != candidate) {
+            // If these suggested words have been generated with out of date input pointers, then
+            // we skip auto-commit (see comments above on the mSequenceNumber member).
+            if (null != candidate && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
                 if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
                     final String[] commitParts = candidate.mWord.split(" ", 2);
                     batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
@@ -1965,10 +1988,11 @@
                     mSpaceState = SPACE_STATE_PHANTOM;
                     mKeyboardSwitcher.updateShiftState();
                     mWordComposer.setCapitalizedModeAtStartComposingTime(getActualCapsMode());
+                    ++mAutoCommitSequenceNumber;
                 }
             }
         }
-        mInputUpdater.onUpdateBatchInput(batchPointers);
+        mInputUpdater.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
     }
 
     // This method must run in UI Thread.
@@ -2503,7 +2527,7 @@
 
         final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>();
         getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_TYPING,
-                new OnGetSuggestedWordsCallback() {
+                SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
                     @Override
                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
                         holder.set(suggestedWords);
@@ -2518,7 +2542,7 @@
         }
     }
 
-    private void getSuggestedWords(final int sessionId,
+    private void getSuggestedWords(final int sessionId, final int sequenceNumber,
             final OnGetSuggestedWordsCallback callback) {
         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
         final Suggest suggest = mSuggest;
@@ -2543,18 +2567,19 @@
         }
         suggest.getSuggestedWords(mWordComposer, prevWord, keyboard.getProximityInfo(),
                 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
-                additionalFeaturesOptions, sessionId, callback);
+                additionalFeaturesOptions, sessionId, sequenceNumber, callback);
     }
 
     private void getSuggestedWordsOrOlderSuggestionsAsync(final int sessionId,
-            final OnGetSuggestedWordsCallback callback) {
-        mInputUpdater.getSuggestedWords(sessionId, new OnGetSuggestedWordsCallback() {
-            @Override
-            public void onGetSuggestedWords(SuggestedWords suggestedWords) {
-                callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
-                        mWordComposer.getTypedWord(), suggestedWords));
-            }
-        });
+            final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
+        mInputUpdater.getSuggestedWords(sessionId, sequenceNumber,
+                new OnGetSuggestedWordsCallback() {
+                    @Override
+                    public void onGetSuggestedWords(SuggestedWords suggestedWords) {
+                        callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
+                                mWordComposer.getTypedWord(), suggestedWords));
+                    }
+                });
     }
 
     private SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
@@ -2889,30 +2914,30 @@
             // We come here if there weren't any suggestion spans on this word. We will try to
             // compute suggestions for it instead.
             mInputUpdater.getSuggestedWords(Suggest.SESSION_TYPING,
-                    new OnGetSuggestedWordsCallback() {
-                @Override
-                public void onGetSuggestedWords(
-                        final SuggestedWords suggestedWordsIncludingTypedWord) {
-                    final SuggestedWords suggestedWords;
-                    if (suggestedWordsIncludingTypedWord.size() > 1) {
-                        // We were able to compute new suggestions for this word.
-                        // Remove the typed word, since we don't want to display it in this case.
-                        // The #getSuggestedWordsExcludingTypedWord() method sets willAutoCorrect to
-                        // false.
-                        suggestedWords = suggestedWordsIncludingTypedWord
-                                .getSuggestedWordsExcludingTypedWord();
-                    } else {
-                        // No saved suggestions, and we were unable to compute any good one either.
-                        // Rather than displaying an empty suggestion strip, we'll display the
-                        // original word alone in the middle.
-                        // Since there is only one word, willAutoCorrect is false.
-                        suggestedWords = suggestedWordsIncludingTypedWord;
-                    }
-                    // We need to pass typedWord because mWordComposer.mTypedWord may differ from
-                    // typedWord.
-                    unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(suggestedWords,
-                        typedWord);
-                }});
+                    SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
+                        @Override
+                        public void onGetSuggestedWords(
+                                final SuggestedWords suggestedWordsIncludingTypedWord) {
+                            final SuggestedWords suggestedWords;
+                            if (suggestedWordsIncludingTypedWord.size() > 1) {
+                                // We were able to compute new suggestions for this word.
+                                // Remove the typed word, since we don't want to display it in this
+                                // case. The #getSuggestedWordsExcludingTypedWord() method sets
+                                // willAutoCorrect to false.
+                                suggestedWords = suggestedWordsIncludingTypedWord
+                                        .getSuggestedWordsExcludingTypedWord();
+                            } else {
+                                // No saved suggestions, and we were unable to compute any good one
+                                // either. Rather than displaying an empty suggestion strip, we'll
+                                // display the original word alone in the middle.
+                                // Since there is only one word, willAutoCorrect is false.
+                                suggestedWords = suggestedWordsIncludingTypedWord;
+                            }
+                            // We need to pass typedWord because mWordComposer.mTypedWord may
+                            // differ from typedWord.
+                            unsetIsAutoCorrectionIndicatorOnAndCallShowSuggestionStrip(
+                                    suggestedWords, typedWord);
+                        }});
         } else {
             // We found suggestion spans in the word. We'll create the SuggestedWords out of
             // them, and make willAutoCorrect false.
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index c270d47..72b9c41 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -217,15 +217,17 @@
     public void getSuggestedWords(final WordComposer wordComposer,
             final String prevWordForBigram, final ProximityInfo proximityInfo,
             final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
-            final int[] additionalFeaturesOptions, final int sessionId,
+            final int[] additionalFeaturesOptions, final int sessionId, final int sequenceNumber,
             final OnGetSuggestedWordsCallback callback) {
         LatinImeLogger.onStartSuggestion(prevWordForBigram);
         if (wordComposer.isBatchMode()) {
             getSuggestedWordsForBatchInput(wordComposer, prevWordForBigram, proximityInfo,
-                    blockOffensiveWords, additionalFeaturesOptions, sessionId, callback);
+                    blockOffensiveWords, additionalFeaturesOptions, sessionId, sequenceNumber,
+                    callback);
         } else {
             getSuggestedWordsForTypingInput(wordComposer, prevWordForBigram, proximityInfo,
-                    blockOffensiveWords, isCorrectionEnabled, additionalFeaturesOptions, callback);
+                    blockOffensiveWords, isCorrectionEnabled, additionalFeaturesOptions,
+                    sequenceNumber, callback);
         }
     }
 
@@ -234,7 +236,8 @@
     private void getSuggestedWordsForTypingInput(final WordComposer wordComposer,
             final String prevWordForBigram, final ProximityInfo proximityInfo,
             final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
-            final int[] additionalFeaturesOptions, final OnGetSuggestedWordsCallback callback) {
+            final int[] additionalFeaturesOptions, final int sequenceNumber,
+            final OnGetSuggestedWordsCallback callback) {
         final int trailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
         final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
                 MAX_SUGGESTIONS);
@@ -347,7 +350,7 @@
                 hasAutoCorrection, /* willAutoCorrect */
                 false /* isPunctuationSuggestions */,
                 false /* isObsoleteSuggestions */,
-                !wordComposer.isComposingWord() /* isPrediction */));
+                !wordComposer.isComposingWord() /* isPrediction */, sequenceNumber));
     }
 
     // Retrieves suggestions for the batch input
@@ -355,7 +358,8 @@
     private void getSuggestedWordsForBatchInput(final WordComposer wordComposer,
             final String prevWordForBigram, final ProximityInfo proximityInfo,
             final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
-            final int sessionId, final OnGetSuggestedWordsCallback callback) {
+            final int sessionId, final int sequenceNumber,
+            final OnGetSuggestedWordsCallback callback) {
         final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
                 MAX_SUGGESTIONS);
 
@@ -408,7 +412,7 @@
                 false /* willAutoCorrect */,
                 false /* isPunctuationSuggestions */,
                 false /* isObsoleteSuggestions */,
-                false /* isPrediction */));
+                false /* isPrediction */, sequenceNumber));
     }
 
     private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index fed4cdb..97c89dd4 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -29,6 +29,7 @@
 public final class SuggestedWords {
     public static final int INDEX_OF_TYPED_WORD = 0;
     public static final int INDEX_OF_AUTO_CORRECTION = 1;
+    public static final int NOT_A_SEQUENCE_NUMBER = -1;
 
     private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST =
             CollectionUtils.newArrayList(0);
@@ -43,6 +44,7 @@
     public final boolean mIsPunctuationSuggestions;
     public final boolean mIsObsoleteSuggestions;
     public final boolean mIsPrediction;
+    public final int mSequenceNumber; // Sequence number for auto-commit.
     private final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList;
 
     public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
@@ -51,12 +53,24 @@
             final boolean isPunctuationSuggestions,
             final boolean isObsoleteSuggestions,
             final boolean isPrediction) {
+        this(suggestedWordInfoList, typedWordValid, willAutoCorrect, isPunctuationSuggestions,
+                isObsoleteSuggestions, isPrediction, NOT_A_SEQUENCE_NUMBER);
+    }
+
+    public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
+            final boolean typedWordValid,
+            final boolean willAutoCorrect,
+            final boolean isPunctuationSuggestions,
+            final boolean isObsoleteSuggestions,
+            final boolean isPrediction,
+            final int sequenceNumber) {
         mSuggestedWordInfoList = suggestedWordInfoList;
         mTypedWordValid = typedWordValid;
         mWillAutoCorrect = willAutoCorrect;
         mIsPunctuationSuggestions = isPunctuationSuggestions;
         mIsObsoleteSuggestions = isObsoleteSuggestions;
         mIsPrediction = isPrediction;
+        mSequenceNumber = sequenceNumber;
     }
 
     public boolean isEmpty() {