Merge "Fix the bottom row of tablet keyboard layout" into lmp-dev
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index ea63cef..0355576 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -16,9 +16,13 @@
 
 package com.android.inputmethod.latin;
 
+import android.graphics.Color;
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
 import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
@@ -81,6 +85,18 @@
      */
     private final StringBuilder mComposingText = new StringBuilder();
 
+    /**
+     * This variable is a temporary object used in
+     * {@link #commitTextWithBackgroundColor(CharSequence, int, int)} to avoid object creation.
+     */
+    private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder();
+    /**
+     * This variable is used to track whether the last committed text had the background color or
+     * not.
+     * TODO: Omit this flag if possible.
+     */
+    private boolean mLastCommittedTextHasBackgroundColor = false;
+
     private final InputMethodService mParent;
     InputConnection mIC;
     int mNestLevel;
@@ -219,12 +235,37 @@
         // it works, but it's wrong and should be fixed.
         mCommittedTextBeforeComposingText.append(mComposingText);
         mComposingText.setLength(0);
+        // TODO: Clear this flag in setComposingRegion() and setComposingText() as well if needed.
+        mLastCommittedTextHasBackgroundColor = false;
         if (null != mIC) {
             mIC.finishComposingText();
         }
     }
 
-    public void commitText(final CharSequence text, final int i) {
+    /**
+     * Synonym of {@code commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT}.
+     * @param text The text to commit. This may include styles.
+     * See {@link InputConnection#commitText(CharSequence, int)}.
+     * @param newCursorPosition The new cursor position around the text.
+     * See {@link InputConnection#commitText(CharSequence, int)}.
+     */
+    public void commitText(final CharSequence text, final int newCursorPosition) {
+        commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT);
+    }
+
+    /**
+     * Calls {@link InputConnection#commitText(CharSequence, int)} with the given background color.
+     * @param text The text to commit. This may include styles.
+     * See {@link InputConnection#commitText(CharSequence, int)}.
+     * @param newCursorPosition The new cursor position around the text.
+     * See {@link InputConnection#commitText(CharSequence, int)}.
+     * @param color The background color to be attached. Set {@link Color#TRANSPARENT} to disable
+     * the background color. Note that this method specifies {@link BackgroundColorSpan} with
+     * {@link Spanned#SPAN_COMPOSING} flag, meaning that the background color persists until
+     * {@link #finishComposingText()} is called.
+     */
+    public void commitTextWithBackgroundColor(final CharSequence text, final int newCursorPosition,
+            final int color) {
         if (DEBUG_BATCH_NESTING) checkBatchEdit();
         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
         mCommittedTextBeforeComposingText.append(text);
@@ -234,11 +275,43 @@
         mExpectedSelStart += text.length() - mComposingText.length();
         mExpectedSelEnd = mExpectedSelStart;
         mComposingText.setLength(0);
+        mLastCommittedTextHasBackgroundColor = false;
         if (null != mIC) {
-            mIC.commitText(text, i);
+            if (color == Color.TRANSPARENT) {
+                mIC.commitText(text, newCursorPosition);
+            } else {
+                mTempObjectForCommitText.clear();
+                mTempObjectForCommitText.append(text);
+                final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(color);
+                mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, text.length(),
+                        Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+                mIC.commitText(mTempObjectForCommitText, newCursorPosition);
+                mLastCommittedTextHasBackgroundColor = true;
+            }
         }
     }
 
+    /**
+     * Removes the background color from the highlighted text if necessary. Should be called while
+     * there is no on-going composing text.
+     *
+     * <p>CAVEAT: This method internally calls {@link InputConnection#finishComposingText()}.
+     * Be careful of any unexpected side effects.</p>
+     */
+    public void removeBackgroundColorFromHighlightedTextIfNecessary() {
+        // TODO: We haven't yet full tested if we really need to check this flag or not. Omit this
+        // flag if everything works fine without this condition.
+        if (!mLastCommittedTextHasBackgroundColor) {
+            return;
+        }
+        if (mComposingText.length() > 0) {
+            Log.e(TAG, "clearSpansWithComposingFlags should be called when composing text is " +
+                    "empty. mComposingText=" + mComposingText);
+            return;
+        }
+        finishComposingText();
+    }
+
     public CharSequence getSelectedText(final int flags) {
         return (null == mIC) ? null : mIC.getSelectedText(flags);
     }
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index d7693af..38fcb68 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -19,6 +19,7 @@
 import android.text.TextUtils;
 import android.view.inputmethod.CompletionInfo;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.define.DebugFlags;
 import com.android.inputmethod.latin.utils.StringUtils;
 
@@ -420,4 +421,18 @@
                 mWillAutoCorrect, mIsObsoleteSuggestions, mIsPrediction,
                 INPUT_STYLE_TAIL_BATCH);
     }
+
+    /**
+     * @return the {@link SuggestedWordInfo} which corresponds to the word that is originally
+     * typed by the user. Otherwise returns {@code null}. Note that gesture input is not
+     * considered to be a typed word.
+     */
+    @UsedForTesting
+    public SuggestedWordInfo getTypedWordInfoOrNull() {
+        if (this == EMPTY) {
+            return null;
+        }
+        final SuggestedWordInfo info = getInfo(SuggestedWords.INDEX_OF_TYPED_WORD);
+        return (info.getKind() == SuggestedWordInfo.KIND_TYPED) ? info : null;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 348bae6..6fce249 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin.inputlogic;
 
+import android.graphics.Color;
 import android.os.SystemClock;
 import android.text.SpannableString;
 import android.text.TextUtils;
@@ -1994,7 +1995,9 @@
     }
 
     /**
-     * Commits the chosen word to the text field and saves it for later retrieval.
+     * Commits the chosen word to the text field and saves it for later retrieval. This is a
+     * synonym of {@code commitChosenWordWithBackgroundColor(settingsValues, chosenWord,
+     * commitType, separatorString, Color.TRANSPARENT}.
      *
      * @param settingsValues the current values of the settings.
      * @param chosenWord the word we want to commit.
@@ -2003,6 +2006,23 @@
      */
     private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
             final int commitType, final String separatorString) {
+        commitChosenWordWithBackgroundColor(settingsValues, chosenWord, commitType, separatorString,
+                Color.TRANSPARENT);
+    }
+
+    /**
+     * Commits the chosen word to the text field and saves it for later retrieval.
+     *
+     * @param settingsValues the current values of the settings.
+     * @param chosenWord the word we want to commit.
+     * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_*
+     * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none.
+     * @param backgroundColor the background color to be specified with the committed text. Pass
+     * {@link Color#TRANSPARENT} to not specify the background color.
+     */
+    private void commitChosenWordWithBackgroundColor(final SettingsValues settingsValues,
+            final String chosenWord, final int commitType, final String separatorString,
+            final int backgroundColor) {
         final SuggestedWords suggestedWords = mSuggestedWords;
         final CharSequence chosenWordWithSuggestions =
                 SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
@@ -2012,7 +2032,7 @@
         // information from the 1st previous word.
         final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord(
                 settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1);
-        mConnection.commitText(chosenWordWithSuggestions, 1);
+        mConnection.commitTextWithBackgroundColor(chosenWordWithSuggestions, 1, backgroundColor);
         // Add the word to the user history dictionary
         performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWordsInfo);
         // TODO: figure out here if this is an auto-correct or if the best word is actually
diff --git a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
index a5f20b5..2785dec 100644
--- a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
+++ b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
@@ -23,24 +23,49 @@
 
 import java.util.ArrayList;
 import java.util.Locale;
-import java.util.Random;
 
 @SmallTest
 public class SuggestedWordsTests extends AndroidTestCase {
+
+    /**
+     * Helper method to create a dummy {@link SuggestedWordInfo} with specifying
+     * {@link SuggestedWordInfo#KIND_TYPED}.
+     *
+     * @param word the word to be used to create {@link SuggestedWordInfo}.
+     * @return a new instance of {@link SuggestedWordInfo}.
+     */
+    private static SuggestedWordInfo createTypedWordInfo(final String word) {
+        // Use 100 as the frequency because the numerical value does not matter as
+        // long as it's > 1 and < INT_MAX.
+        return new SuggestedWordInfo(word, 100 /* score */,
+                SuggestedWordInfo.KIND_TYPED,
+                null /* sourceDict */,
+                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                1 /* autoCommitFirstWordConfidence */);
+    }
+
+    /**
+     * Helper method to create a dummy {@link SuggestedWordInfo} with specifying
+     * {@link SuggestedWordInfo#KIND_CORRECTION}.
+     *
+     * @param word the word to be used to create {@link SuggestedWordInfo}.
+     * @return a new instance of {@link SuggestedWordInfo}.
+     */
+    private static SuggestedWordInfo createCorrectionWordInfo(final String word) {
+        return new SuggestedWordInfo(word, 1 /* score */,
+                SuggestedWordInfo.KIND_CORRECTION,
+                null /* sourceDict */,
+                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */);
+    }
+
     public void testGetSuggestedWordsExcludingTypedWord() {
         final String TYPED_WORD = "typed";
-        final int TYPED_WORD_FREQ = 5;
         final int NUMBER_OF_ADDED_SUGGESTIONS = 5;
         final ArrayList<SuggestedWordInfo> list = new ArrayList<>();
-        list.add(new SuggestedWordInfo(TYPED_WORD, TYPED_WORD_FREQ,
-                SuggestedWordInfo.KIND_TYPED, null /* sourceDict */,
-                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
-                SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
+        list.add(createTypedWordInfo(TYPED_WORD));
         for (int i = 0; i < NUMBER_OF_ADDED_SUGGESTIONS; ++i) {
-            list.add(new SuggestedWordInfo("" + i, 1, SuggestedWordInfo.KIND_CORRECTION,
-                    null /* sourceDict */,
-                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
-                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
+            list.add(createCorrectionWordInfo(Integer.toString(i)));
         }
 
         final SuggestedWords words = new SuggestedWords(
@@ -66,19 +91,9 @@
     }
 
     // Helper for testGetTransformedWordInfo
-    private SuggestedWordInfo createWordInfo(final String s) {
-        // Use 100 as the frequency because the numerical value does not matter as
-        // long as it's > 1 and < INT_MAX.
-        return new SuggestedWordInfo(s, 100,
-                SuggestedWordInfo.KIND_TYPED, null /* sourceDict */,
-                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
-                new Random().nextInt(1000000) /* autoCommitFirstWordConfidence */);
-    }
-
-    // Helper for testGetTransformedWordInfo
     private SuggestedWordInfo transformWordInfo(final String info,
             final int trailingSingleQuotesCount) {
-        final SuggestedWordInfo suggestedWordInfo = createWordInfo(info);
+        final SuggestedWordInfo suggestedWordInfo = createTypedWordInfo(info);
         final SuggestedWordInfo returnedWordInfo =
                 Suggest.getTransformedSuggestedWordInfo(suggestedWordInfo,
                 Locale.ENGLISH, false /* isAllUpperCase */, false /* isFirstCharCapitalized */,
@@ -102,4 +117,35 @@
         result = transformWordInfo("didn't", 3);
         assertEquals(result.mWord, "didn't''");
     }
+
+    public void testGetTypedWordInfoOrNull() {
+        final String TYPED_WORD = "typed";
+        final int NUMBER_OF_ADDED_SUGGESTIONS = 5;
+        final ArrayList<SuggestedWordInfo> list = new ArrayList<>();
+        list.add(createTypedWordInfo(TYPED_WORD));
+        for (int i = 0; i < NUMBER_OF_ADDED_SUGGESTIONS; ++i) {
+            list.add(createCorrectionWordInfo(Integer.toString(i)));
+        }
+
+        // Make sure getTypedWordInfoOrNull() returns non-null object.
+        final SuggestedWords wordsWithTypedWord = new SuggestedWords(
+                list, null /* rawSuggestions */,
+                false /* typedWordValid */,
+                false /* willAutoCorrect */,
+                false /* isObsoleteSuggestions */,
+                false /* isPrediction*/,
+                SuggestedWords.INPUT_STYLE_NONE);
+        final SuggestedWordInfo typedWord = wordsWithTypedWord.getTypedWordInfoOrNull();
+        assertNotNull(typedWord);
+        assertEquals(TYPED_WORD, typedWord.mWord);
+
+        // Make sure getTypedWordInfoOrNull() returns null.
+        final SuggestedWords wordsWithoutTypedWord =
+                wordsWithTypedWord.getSuggestedWordsExcludingTypedWord(
+                        SuggestedWords.INPUT_STYLE_NONE);
+        assertNull(wordsWithoutTypedWord.getTypedWordInfoOrNull());
+
+        // Make sure getTypedWordInfoOrNull() returns null.
+        assertNull(SuggestedWords.EMPTY.getTypedWordInfoOrNull());
+    }
 }