Merge "Separate string resource of IME name"
diff --git a/java/src/com/android/inputmethod/latin/InputPointers.java b/java/src/com/android/inputmethod/latin/InputPointers.java
index 218243e..9d77d4e 100644
--- a/java/src/com/android/inputmethod/latin/InputPointers.java
+++ b/java/src/com/android/inputmethod/latin/InputPointers.java
@@ -18,6 +18,7 @@
 
 import java.util.Arrays;
 
+// TODO: Add unit test
 public class InputPointers {
     private final ScalableIntArray mXCoordinates = new ScalableIntArray();
     private final ScalableIntArray mYCoordinates = new ScalableIntArray();
@@ -52,6 +53,25 @@
         mTimes.copy(ip.mTimes);
     }
 
+    /**
+     * Append the pointers in the specified {@link InputPointers} to the end of this.
+     * @param src the source {@link InputPointers} to append the pointers.
+     * @param startPos the starting index of the pointers in {@code src}.
+     * @param length the number of pointers to be appended.
+     */
+    public void append(InputPointers src, int startPos, int length) {
+        final int currentLength = getPointerSize();
+        final int newLength = currentLength + length;
+        mXCoordinates.ensureCapacity(newLength);
+        mYCoordinates.ensureCapacity(newLength);
+        mPointerIds.ensureCapacity(newLength);
+        mTimes.ensureCapacity(newLength);
+        System.arraycopy(src.getXCoordinates(), startPos, getXCoordinates(), currentLength, length);
+        System.arraycopy(src.getYCoordinates(), startPos, getYCoordinates(), currentLength, length);
+        System.arraycopy(src.getPointerIds(), startPos, getPointerIds(), currentLength, length);
+        System.arraycopy(src.getTimes(), startPos, getTimes(), currentLength, length);
+    }
+
     public void reset() {
         mXCoordinates.reset();
         mYCoordinates.reset();
@@ -64,19 +84,19 @@
     }
 
     public int[] getXCoordinates() {
-        return mXCoordinates.mArray;
+        return mXCoordinates.getPrimitiveArray();
     }
 
     public int[] getYCoordinates() {
-        return mYCoordinates.mArray;
+        return mYCoordinates.getPrimitiveArray();
     }
 
     public int[] getPointerIds() {
-        return mPointerIds.mArray;
+        return mPointerIds.getPrimitiveArray();
     }
 
     public int[] getTimes() {
-        return mTimes.mArray;
+        return mTimes.getPrimitiveArray();
     }
 
     private static class ScalableIntArray {
@@ -98,14 +118,24 @@
         }
 
         public void add(int val) {
-            if (mLength >= mArray.length) {
-                final int[] newArray = new int[mLength * 2];
-                System.arraycopy(mArray, 0, newArray, 0, mLength);
-            }
+            ensureCapacity(mLength);
             mArray[mLength] = val;
             ++mLength;
         }
 
+        public void ensureCapacity(int minimumCapacity) {
+            if (mArray.length < minimumCapacity) {
+                final int nextCapacity = mArray.length * 2;
+                grow(minimumCapacity > nextCapacity ? minimumCapacity : nextCapacity);
+            }
+        }
+
+        private void grow(int newCapacity) {
+            final int[] newArray = new int[newCapacity];
+            System.arraycopy(mArray, 0, newArray, 0, mLength);
+            mArray = newArray;
+        }
+
         public int getLength() {
             return mLength;
         }
@@ -121,6 +151,7 @@
 
         public void copy(ScalableIntArray ip) {
             mArray = Arrays.copyOf(ip.mArray, ip.mArray.length);
+            mLength = ip.mLength;
         }
 
         public void set(ScalableIntArray ip) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 243a5f6..4be2a17 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -177,9 +177,8 @@
 
     public static class UIHandler extends StaticInnerHandlerWrapper<LatinIME> {
         private static final int MSG_UPDATE_SHIFT_STATE = 1;
-        private static final int MSG_SET_BIGRAM_PREDICTIONS = 5;
         private static final int MSG_PENDING_IMS_CALLBACK = 6;
-        private static final int MSG_UPDATE_SUGGESTIONS = 7;
+        private static final int MSG_UPDATE_SUGGESTION_STRIP = 7;
 
         private int mDelayUpdateSuggestions;
         private int mDelayUpdateShiftState;
@@ -205,30 +204,25 @@
             final LatinIME latinIme = getOuterInstance();
             final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
             switch (msg.what) {
-            case MSG_UPDATE_SUGGESTIONS:
-                latinIme.updateSuggestionsOrPredictions(false /* isPredictions */);
+            case MSG_UPDATE_SUGGESTION_STRIP:
+                latinIme.updateSuggestionsOrPredictions();
                 break;
             case MSG_UPDATE_SHIFT_STATE:
                 switcher.updateShiftState();
                 break;
-            case MSG_SET_BIGRAM_PREDICTIONS:
-                latinIme.updateSuggestionsOrPredictions(true /* isPredictions */);
-                break;
             }
         }
 
-        public void postUpdateSuggestions() {
-            cancelUpdateSuggestionStrip();
-            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTIONS), mDelayUpdateSuggestions);
+        public void postUpdateSuggestionStrip() {
+            sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP), mDelayUpdateSuggestions);
         }
 
         public void cancelUpdateSuggestionStrip() {
-            removeMessages(MSG_UPDATE_SUGGESTIONS);
-            removeMessages(MSG_SET_BIGRAM_PREDICTIONS);
+            removeMessages(MSG_UPDATE_SUGGESTION_STRIP);
         }
 
         public boolean hasPendingUpdateSuggestions() {
-            return hasMessages(MSG_UPDATE_SUGGESTIONS);
+            return hasMessages(MSG_UPDATE_SUGGESTION_STRIP);
         }
 
         public void postUpdateShiftState() {
@@ -240,11 +234,6 @@
             removeMessages(MSG_UPDATE_SHIFT_STATE);
         }
 
-        public void postUpdateBigramPredictions() {
-            cancelUpdateSuggestionStrip();
-            sendMessageDelayed(obtainMessage(MSG_SET_BIGRAM_PREDICTIONS), mDelayUpdateSuggestions);
-        }
-
         public void startDoubleSpacesTimer() {
             mDoubleSpaceTimerStart = SystemClock.uptimeMillis();
         }
@@ -689,8 +678,8 @@
             mSuggestionsView.clear();
         setSuggestionStripShownInternal(
                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
-        // Delay updating suggestions because keyboard input view may not be shown at this point.
-        mHandler.postUpdateSuggestions();
+
+        mHandler.cancelUpdateSuggestionStrip();
         mHandler.cancelDoubleSpacesTimer();
 
         inputView.setKeyPreviewPopupEnabled(mCurrentSettings.mKeyPreviewPopupOn,
@@ -1021,7 +1010,7 @@
                     LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, typedWord.toString(),
                     separatorCode, prevWord);
         }
-        updateSuggestionsOrPredictions(false /* isPredictions */);
+        updateSuggestionsOrPredictions();
     }
 
     public int getCurrentAutoCapsState() {
@@ -1394,15 +1383,7 @@
                     mWordComposer.deleteLast();
                 }
                 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
-                // If we have deleted the last remaining character of a word, then we are not
-                // isComposingWord() any more.
-                if (!mWordComposer.isComposingWord()) {
-                    // Not composing word any more, so we can show bigrams.
-                    mHandler.postUpdateBigramPredictions();
-                } else {
-                    // Still composing a word, so we still have letters to deduce a suggestion from.
-                    mHandler.postUpdateSuggestions();
-                }
+                mHandler.postUpdateSuggestionStrip();
             } else {
                 mConnection.deleteSurroundingText(1, 0);
                 if (ProductionFlag.IS_EXPERIMENTAL) {
@@ -1545,7 +1526,6 @@
                         getCurrentAutoCapsState() != Constants.TextUtils.CAP_MODE_OFF);
             }
             mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
-            mHandler.postUpdateSuggestions();
         } else {
             final boolean swapWeakSpace = maybeStripSpace(primaryCode,
                     spaceState, KeyboardActionListener.SUGGESTION_STRIP_COORDINATE == x);
@@ -1556,11 +1536,10 @@
                 swapSwapperAndSpace();
                 mSpaceState = SPACE_STATE_WEAK;
             }
-            // We may need to update predictions, if the "add to dictionary" hint was displayed
-            // for example.
+            // In case the "add to dictionary" hint was still displayed.
             if (null != mSuggestionsView) mSuggestionsView.dismissAddToDictionaryHint();
-            mHandler.postUpdateBigramPredictions();
         }
+        mHandler.postUpdateSuggestionStrip();
         Utils.Stats.onNonSeparator((char)primaryCode, x, y);
     }
 
@@ -1602,7 +1581,7 @@
 
             mHandler.startDoubleSpacesTimer();
             if (!mConnection.isCursorTouchingWord(mCurrentSettings)) {
-                mHandler.postUpdateBigramPredictions();
+                mHandler.postUpdateSuggestionStrip();
             }
         } else {
             if (swapWeakSpace) {
@@ -1682,7 +1661,7 @@
         }
     }
 
-    public void updateSuggestionsOrPredictions(final boolean isPredictions) {
+    public void updateSuggestionsOrPredictions() {
         mHandler.cancelUpdateSuggestionStrip();
 
         // Check if we have a suggestion engine attached.
@@ -1695,17 +1674,16 @@
             return;
         }
 
-        final CharSequence typedWord;
+        final String typedWord = mWordComposer.getTypedWord();
         final SuggestedWords suggestions;
-        if (isPredictions || !mWordComposer.isComposingWord()) {
-            if (!mCurrentSettings.mBigramPredictionEnabled) {
-                setPunctuationSuggestions();
-                return;
-            }
-            typedWord = "";
-            suggestions = updateBigramPredictions(typedWord);
+        if (!mWordComposer.isComposingWord() && !mCurrentSettings.mBigramPredictionEnabled) {
+            setPunctuationSuggestions();
+            return;
+        }
+
+        if (!mWordComposer.isComposingWord()) {
+            suggestions = updateBigramPredictions();
         } else {
-            typedWord = mWordComposer.getTypedWord();
             suggestions = updateSuggestions(typedWord);
         }
 
@@ -1718,11 +1696,13 @@
 
     private SuggestedWords updateSuggestions(final CharSequence typedWord) {
         // TODO: May need a better way of retrieving previous word
-        final CharSequence prevWord = mConnection.getPreviousWord(mCurrentSettings.mWordSeparators);
+        final CharSequence prevWord =
+                mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2);
         // getSuggestedWords handles gracefully a null value of prevWord
         final SuggestedWords suggestedWords = mSuggest.getSuggestedWords(mWordComposer,
                 prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(),
-                mCurrentSettings.mCorrectionEnabled, false);
+                // !mWordComposer.isComposingWord() is known to be false
+                mCurrentSettings.mCorrectionEnabled, !mWordComposer.isComposingWord());
 
         // Basically, we update the suggestion strip only when suggestion count > 1.  However,
         // there is an exception: We update the suggestion strip whenever typed word's length
@@ -1774,8 +1754,7 @@
     private void commitCurrentAutoCorrection(final int separatorCodePoint) {
         // Complete any pending suggestions query first
         if (mHandler.hasPendingUpdateSuggestions()) {
-            mHandler.cancelUpdateSuggestionStrip();
-            updateSuggestionsOrPredictions(false /* isPredictions */);
+            updateSuggestionsOrPredictions();
         }
         final CharSequence autoCorrection = mWordComposer.getAutoCorrectionOrNull();
         if (autoCorrection != null) {
@@ -1886,8 +1865,8 @@
         if (showingAddToDictionaryHint && mIsUserDictionaryAvailable) {
             mSuggestionsView.showAddToDictionaryHint(suggestion, mCurrentSettings.mHintToSaveText);
         } else {
-            // If we're not showing the "Touch again to save", then show predictions.
-            mHandler.postUpdateBigramPredictions();
+            // If we're not showing the "Touch again to save", then update the suggestion strip.
+            mHandler.postUpdateSuggestionStrip();
         }
     }
 
@@ -1912,11 +1891,13 @@
                 separatorCode, prevWord);
     }
 
-    private SuggestedWords updateBigramPredictions(final CharSequence typedWord) {
-        final CharSequence prevWord = mConnection.getThisWord(mCurrentSettings.mWordSeparators);
+    private SuggestedWords updateBigramPredictions() {
+        final CharSequence prevWord =
+                mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 1);
         return mSuggest.getSuggestedWords(mWordComposer,
                 prevWord, mKeyboardSwitcher.getKeyboard().getProximityInfo(),
-                mCurrentSettings.mCorrectionEnabled, true);
+                // !mWordComposer.isComposingWord() is known to be true
+                mCurrentSettings.mCorrectionEnabled, !mWordComposer.isComposingWord());
     }
 
     public void setPunctuationSuggestions() {
@@ -1940,7 +1921,7 @@
         final UserHistoryDictionary userHistoryDictionary = mUserHistoryDictionary;
         if (userHistoryDictionary != null) {
             final CharSequence prevWord
-                    = mConnection.getPreviousWord(mCurrentSettings.mWordSeparators);
+                    = mConnection.getNthPreviousWord(mCurrentSettings.mWordSeparators, 2);
             final String secondWord;
             if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
                 secondWord = suggestion.toString().toLowerCase(
@@ -1979,7 +1960,7 @@
             ResearchLogger.latinIME_deleteSurroundingText(length);
         }
         mConnection.setComposingText(word, 1);
-        mHandler.postUpdateSuggestions();
+        mHandler.postUpdateSuggestionStrip();
     }
 
     private void revertCommit() {
@@ -2024,7 +2005,7 @@
         // separator.
         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
         // We have a separator between the word and the cursor: we should show predictions.
-        mHandler.postUpdateBigramPredictions();
+        mHandler.postUpdateSuggestionStrip();
     }
 
     public boolean isWordSeparator(int code) {
@@ -2048,12 +2029,8 @@
         loadSettings();
         // Since we just changed languages, we should re-evaluate suggestions with whatever word
         // we are currently composing. If we are not composing anything, we may want to display
-        // predictions or punctuation signs (which is done by updateBigramPredictions anyway).
-        if (mWordComposer.isComposingWord()) {
-            mHandler.postUpdateSuggestions();
-        } else {
-            mHandler.postUpdateBigramPredictions();
-        }
+        // predictions or punctuation signs (which is done by the updateSuggestionStrip anyway).
+        mHandler.postUpdateSuggestionStrip();
     }
 
     // TODO: Remove this method from {@link LatinIME} and move {@link FeedbackManager} to
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index a37f480..5786978 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -139,12 +139,11 @@
         if (null != mIC) mIC.commitCompletion(completionInfo);
     }
 
-    public CharSequence getPreviousWord(final String sentenceSeperators) {
+    public CharSequence getNthPreviousWord(final String sentenceSeperators, final int n) {
         mIC = mParent.getCurrentInputConnection();
-        //TODO: Should fix this. This could be slow!
         if (null == mIC) return null;
-        CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
-        return getPreviousWord(prev, sentenceSeperators);
+        final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
+        return getNthPreviousWord(prev, sentenceSeperators, n);
     }
 
     /**
@@ -177,56 +176,35 @@
         return sep.indexOf(code) != -1;
     }
 
-    // Get the word before the whitespace preceding the non-whitespace preceding the cursor.
-    // Also, it won't return words that end in a separator.
+    // Get the nth word before cursor. n = 1 retrieves the word immediately before the cursor,
+    // n = 2 retrieves the word before that, and so on. This splits on whitespace only.
+    // Also, it won't return words that end in a separator (if the nth word before the cursor
+    // ends in a separator, it returns null).
     // Example :
-    // "abc def|" -> abc
-    // "abc def |" -> abc
-    // "abc def. |" -> abc
-    // "abc def . |" -> def
-    // "abc|" -> null
-    // "abc |" -> null
-    // "abc. def|" -> null
-    public static CharSequence getPreviousWord(CharSequence prev, String sentenceSeperators) {
+    // (n = 1) "abc def|" -> def
+    // (n = 1) "abc def |" -> def
+    // (n = 1) "abc def. |" -> null
+    // (n = 1) "abc def . |" -> null
+    // (n = 2) "abc def|" -> abc
+    // (n = 2) "abc def |" -> abc
+    // (n = 2) "abc def. |" -> abc
+    // (n = 2) "abc def . |" -> def
+    // (n = 2) "abc|" -> null
+    // (n = 2) "abc |" -> null
+    // (n = 2) "abc. def|" -> null
+    public static CharSequence getNthPreviousWord(final CharSequence prev,
+            final String sentenceSeperators, final int n) {
         if (prev == null) return null;
         String[] w = spaceRegex.split(prev);
 
-        // If we can't find two words, or we found an empty word, return null.
-        if (w.length < 2 || w[w.length - 2].length() <= 0) return null;
+        // If we can't find n words, or we found an empty word, return null.
+        if (w.length < n || w[w.length - n].length() <= 0) return null;
 
         // If ends in a separator, return null
-        char lastChar = w[w.length - 2].charAt(w[w.length - 2].length() - 1);
+        char lastChar = w[w.length - n].charAt(w[w.length - n].length() - 1);
         if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
 
-        return w[w.length - 2];
-    }
-
-    public CharSequence getThisWord(String sentenceSeperators) {
-        mIC = mParent.getCurrentInputConnection();
-        if (null == mIC) return null;
-        final CharSequence prev = mIC.getTextBeforeCursor(LOOKBACK_CHARACTER_NUM, 0);
-        return getThisWord(prev, sentenceSeperators);
-    }
-
-    // Get the word immediately before the cursor, even if there is whitespace between it and
-    // the cursor - but not if there is punctuation.
-    // Example :
-    // "abc def|" -> def
-    // "abc def |" -> def
-    // "abc def. |" -> null
-    // "abc def . |" -> null
-    public static CharSequence getThisWord(CharSequence prev, String sentenceSeperators) {
-        if (prev == null) return null;
-        String[] w = spaceRegex.split(prev);
-
-        // No word : return null
-        if (w.length < 1 || w[w.length - 1].length() <= 0) return null;
-
-        // If ends in a separator, return null
-        char lastChar = w[w.length - 1].charAt(w[w.length - 1].length() - 1);
-        if (sentenceSeperators.contains(String.valueOf(lastChar))) return null;
-
-        return w[w.length - 1];
+        return w[w.length - n];
     }
 
     /**
diff --git a/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
index 7bd7b0e..ad99379 100644
--- a/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
+++ b/tests/src/com/android/inputmethod/latin/RichInputConnectionTests.java
@@ -102,31 +102,26 @@
      */
     public void testGetPreviousWord() {
         // If one of the following cases breaks, the bigram suggestions won't work.
-        assertEquals(RichInputConnection.getPreviousWord("abc def", sSeparators), "abc");
-        assertNull(RichInputConnection.getPreviousWord("abc", sSeparators));
-        assertNull(RichInputConnection.getPreviousWord("abc. def", sSeparators));
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSeparators, 2), "abc");
+        assertNull(RichInputConnection.getNthPreviousWord("abc", sSeparators, 2));
+        assertNull(RichInputConnection.getNthPreviousWord("abc. def", sSeparators, 2));
 
         // The following tests reflect the current behavior of the function
-        // RichInputConnection#getPreviousWord.
+        // RichInputConnection#getNthPreviousWord.
         // TODO: However at this time, the code does never go
         // into such a path, so it should be safe to change the behavior of
         // this function if needed - especially since it does not seem very
         // logical. These tests are just there to catch any unintentional
         // changes in the behavior of the RichInputConnection#getPreviousWord method.
-        assertEquals(RichInputConnection.getPreviousWord("abc def ", sSeparators), "abc");
-        assertEquals(RichInputConnection.getPreviousWord("abc def.", sSeparators), "abc");
-        assertEquals(RichInputConnection.getPreviousWord("abc def .", sSeparators), "def");
-        assertNull(RichInputConnection.getPreviousWord("abc ", sSeparators));
-    }
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSeparators, 2), "abc");
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def.", sSeparators, 2), "abc");
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def .", sSeparators, 2), "def");
+        assertNull(RichInputConnection.getNthPreviousWord("abc ", sSeparators, 2));
 
-    /**
-     * Test for getting the word before the cursor (for bigram)
-     */
-    public void testGetThisWord() {
-        assertEquals(RichInputConnection.getThisWord("abc def", sSeparators), "def");
-        assertEquals(RichInputConnection.getThisWord("abc def ", sSeparators), "def");
-        assertNull(RichInputConnection.getThisWord("abc def.", sSeparators));
-        assertNull(RichInputConnection.getThisWord("abc def .", sSeparators));
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def", sSeparators, 1), "def");
+        assertEquals(RichInputConnection.getNthPreviousWord("abc def ", sSeparators, 1), "def");
+        assertNull(RichInputConnection.getNthPreviousWord("abc def.", sSeparators, 1));
+        assertNull(RichInputConnection.getNthPreviousWord("abc def .", sSeparators, 1));
     }
 
     /**