Merge "Make MAX_PREV_WORD_COUNT_FOR_N_GRAM 2."
diff --git a/java-overridable/src/com/android/inputmethod/latin/utils/LoginAccountUtils.java b/java-overridable/src/com/android/inputmethod/latin/utils/LoginAccountUtils.java
new file mode 100644
index 0000000..faada29
--- /dev/null
+++ b/java-overridable/src/com/android/inputmethod/latin/utils/LoginAccountUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.inputmethod.latin.utils;
+
+import android.content.Context;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Utility class for retrieving accounts that may be used for login.
+ */
+public class LoginAccountUtils {
+    private LoginAccountUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    /**
+     * Get the accounts available for login.
+     *
+     * @return an array of accounts. Empty (never null) if no accounts are available for login.
+     */
+    @Nonnull
+    public static String[] getAccountsForLogin(final Context context) {
+        return new String[0];
+    }
+}
diff --git a/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtils.java b/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtils.java
index 2274852..ad34dc2 100644
--- a/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtils.java
+++ b/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtils.java
@@ -69,4 +69,14 @@
     public static void onAutoCorrection(final String typedWord, final String autoCorrectionWord,
             final boolean isBatchInput, @Nullable final String dictionaryType) {
     }
+
+    public static void onWordCommitUserTyped(final String commitWord, final boolean isBatchMode) {
+    }
+
+    public static void onWordCommitAutoCorrect(final String commitWord, final boolean isBatchMode) {
+    }
+
+    public static void onWordCommitSuggestionPickedManually(
+            final String commitWord, final boolean isBatchMode) {
+    }
 }
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index 054c415..b29a6e2 100644
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -158,5 +158,9 @@
                 <action android:name="android.intent.action.MAIN"/>
             </intent-filter>
         </activity>
+
+        <!-- Unexported activity used for tests. -->
+        <activity android:name=".settings.TestFragmentActivity"
+                android:exported="false" />
     </application>
 </manifest>
diff --git a/java/res/values/donottranslate-text-decorator.xml b/java/res/values/donottranslate-text-decorator.xml
index 832610b..2693645 100644
--- a/java/res/values/donottranslate-text-decorator.xml
+++ b/java/res/values/donottranslate-text-decorator.xml
@@ -19,62 +19,11 @@
 -->
 
 <resources>
-    <!-- The delay time in milliseconds from to show the commit indicator -->
-    <integer name="text_decorator_delay_in_milliseconds_to_show_commit_indicator">
-        500
-    </integer>
-
     <!-- The extra margin in dp around the hit area of the commit/add-to-dictionary indicator -->
     <integer name="text_decorator_hit_area_margin_in_dp">
         4
     </integer>
 
-    <!-- If true, the commit/add-to-text indicator will be suppressed when the word isn't going to
-         trigger auto-correction. -->
-    <bool name="text_decorator_only_for_auto_correction">true</bool>
-
-    <!-- If true, the commit/add-to-text indicator will be suppressed when the word is already in
-         the dictionary. -->
-    <bool name="text_decorator_only_for_out_of_vocabulary">false</bool>
-
-    <!-- Background color to be used to highlight the target text when the commit indicator is
-         visible. -->
-    <color name="text_decorator_commit_indicator_text_highlight_color">
-        #B6E2DE
-    </color>
-
-    <!-- Background color of the commit indicator. -->
-    <color name="text_decorator_commit_indicator_background_color">
-        #48B6AC
-    </color>
-
-    <!-- Foreground color of the commit indicator. -->
-    <color name="text_decorator_commit_indicator_foreground_color">
-        #FFFFFF
-    </color>
-
-    <!-- Viewport size of "text_decorator_commit_indicator_path". -->
-    <integer name="text_decorator_commit_indicator_path_size">
-        480
-    </integer>
-
-    <!-- Coordinates of the closed path to be used to render the commit indicator.
-         The format is:  X[0], Y[0], X[1], Y[1], ..., X[N-1], Y[N-1] -->
-    <integer-array name="text_decorator_commit_indicator_path">
-        <item>180</item>
-        <item>323</item>
-        <item>97</item>
-        <item>240</item>
-        <item>68</item>
-        <item>268</item>
-        <item>180</item>
-        <item>380</item>
-        <item>420</item>
-        <item>140</item>
-        <item>392</item>
-        <item>112</item>
-    </integer-array>
-
     <!-- Background color to be used to highlight the target text when the add-to-dictionary
          indicator is visible. -->
     <color name="text_decorator_add_to_dictionary_indicator_text_highlight_color">
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 73fb7bd..d64444e 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -38,6 +38,8 @@
 
     <!-- Settings screen title for preferences [CHAR LIMIT=33]-->
     <string name="settings_screen_preferences">Preferences</string>
+    <!-- Settings screen title for accounts and privacy preferences [CHAR LIMIT=33]-->
+    <string name="settings_screen_accounts">Accounts &amp; privacy</string>
     <!-- Settings screen title for appearance & layouts preferences [CHAR LIMIT=33] -->
     <string name="settings_screen_appearance">Appearance &amp; layouts</string>
     <!-- Settings screen title for multilingual options [CHAR_LIMIT=33] -->
@@ -177,6 +179,23 @@
     <!-- Title of the item to change the keyboard theme [CHAR LIMIT=20]-->
     <string name="keyboard_layout">Keyboard theme</string>
 
+    <!-- Title of the preference item for switching accounts [CHAR LIMIT=30] -->
+    <string name="switch_accounts">Switch accounts</string>
+    <!-- Summary of the preference item for switching accounts when no accounts
+         are selected [CHAR LIMIT=65] -->
+    <string name="no_accounts_selected">No accounts selected</string>
+    <!-- Summary of the preference item for switching accounts when an account
+         is selected [CHAR LIMIT=65] -->
+    <string name="account_selected">Currently using <xliff:g id="EMAIL_ADDRESS" example="someone@example.com">%1$s</xliff:g></string>
+    <!-- Positive text for selecting an account -->
+    <string name="account_select_ok">OK</string>
+    <!-- Negative text for selecting an account -->
+    <string name="account_select_cancel">Cancel</string>
+    <!-- Text for signing out of an account -->
+    <string name="account_select_sign_out">Sign out</string>
+    <!-- Title of the account picker dialog for selecting an account [CHAR LIMIT=40] -->
+    <string name="account_select_title">Select an account to use</string>
+
     <!-- Description for English (UK) keyboard subtype [CHAR LIMIT=25]
          (UK) should be an abbreviation of United Kingdom to fit in the CHAR LIMIT. -->
     <string name="subtype_en_GB">English (UK)</string>
diff --git a/java/res/xml/keyboard_layout_set_qwerty.xml b/java/res/xml/keyboard_layout_set_qwerty.xml
index 1aa6f01..7c9a140 100644
--- a/java/res/xml/keyboard_layout_set_qwerty.xml
+++ b/java/res/xml/keyboard_layout_set_qwerty.xml
@@ -24,7 +24,7 @@
         latin:elementName="alphabet"
         latin:elementKeyboard="@xml/kbd_qwerty"
         latin:enableProximityCharsCorrection="true"
-        latin:supportsSplitLayout="false" />
+        latin:supportsSplitLayout="true" />
     <Element
         latin:elementName="symbols"
         latin:elementKeyboard="@xml/kbd_symbols" />
diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml
index c14cd64..2a5134d 100644
--- a/java/res/xml/prefs.xml
+++ b/java/res/xml/prefs.xml
@@ -23,6 +23,10 @@
         android:title="@string/settings_screen_preferences"
         android:key="screen_preferences" />
     <PreferenceScreen
+        android:fragment="com.android.inputmethod.latin.settings.AccountsSettingsFragment"
+        android:title="@string/settings_screen_accounts"
+        android:key="screen_accounts" />
+    <PreferenceScreen
         android:fragment="com.android.inputmethod.latin.settings.AppearanceSettingsFragment"
         android:title="@string/settings_screen_appearance"
         android:key="screen_appearance" />
diff --git a/java/res/xml/prefs_screen_accounts.xml b/java/res/xml/prefs_screen_accounts.xml
new file mode 100644
index 0000000..b5d526a
--- /dev/null
+++ b/java/res/xml/prefs_screen_accounts.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    android:title="@string/settings_screen_accounts">
+
+    <!-- This preference is a dummy view of the underlying preference.
+         This isn't persisted and the summary/title is refreshed by the fragment
+         after inspecting the underlying account preference. -->
+    <Preference
+        android:key="account_switcher"
+        android:persistent="false"
+        android:title="@string/switch_accounts"
+        android:summary="@string/no_accounts_selected" />
+
+    <!-- title will be set programmatically to embed application name -->
+    <CheckBoxPreference
+        android:key="pref_enable_metrics_logging"
+        android:summary="@string/enable_metrics_logging_summary"
+        android:defaultValue="true"
+        android:persistent="true" />
+</PreferenceScreen>
diff --git a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java
index 8a28185..c937eee 100644
--- a/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java
+++ b/java/src/com/android/inputmethod/compat/CursorAnchorInfoCompatWrapper.java
@@ -41,6 +41,8 @@
 
     // Note that CursorAnchorInfo has been introduced in API level XX (Build.VERSION_CODE.LXX).
     private static final CompatUtils.ClassWrapper sCursorAnchorInfoClass;
+    private static final CompatUtils.ToIntMethodWrapper sGetSelectionStartMethod;
+    private static final CompatUtils.ToIntMethodWrapper sGetSelectionEndMethod;
     private static final CompatUtils.ToObjectMethodWrapper<RectF> sGetCharacterBoundsMethod;
     private static final CompatUtils.ToIntMethodWrapper sGetCharacterBoundsFlagsMethod;
     private static final CompatUtils.ToObjectMethodWrapper<CharSequence> sGetComposingTextMethod;
@@ -52,10 +54,14 @@
     private static final CompatUtils.ToObjectMethodWrapper<Matrix> sGetMatrixMethod;
     private static final CompatUtils.ToIntMethodWrapper sGetInsertionMarkerFlagsMethod;
 
-    private static int COMPOSING_TEXT_START_DEFAULT = -1;
+    private static int INVALID_TEXT_INDEX = -1;
     static {
         sCursorAnchorInfoClass = CompatUtils.getClassWrapper(
                 "android.view.inputmethod.CursorAnchorInfo");
+        sGetSelectionStartMethod = sCursorAnchorInfoClass.getPrimitiveMethod(
+                "getSelectionStart", INVALID_TEXT_INDEX);
+        sGetSelectionEndMethod = sCursorAnchorInfoClass.getPrimitiveMethod(
+                "getSelectionEnd", INVALID_TEXT_INDEX);
         sGetCharacterBoundsMethod = sCursorAnchorInfoClass.getMethod(
                 "getCharacterBounds", (RectF)null, int.class);
         sGetCharacterBoundsFlagsMethod = sCursorAnchorInfoClass.getPrimitiveMethod(
@@ -63,7 +69,7 @@
         sGetComposingTextMethod = sCursorAnchorInfoClass.getMethod(
                 "getComposingText", (CharSequence)null);
         sGetComposingTextStartMethod = sCursorAnchorInfoClass.getPrimitiveMethod(
-                "getComposingTextStart", COMPOSING_TEXT_START_DEFAULT);
+                "getComposingTextStart", INVALID_TEXT_INDEX);
         sGetInsertionMarkerBaselineMethod = sCursorAnchorInfoClass.getPrimitiveMethod(
                 "getInsertionMarkerBaseline", 0.0f);
         sGetInsertionMarkerBottomMethod = sCursorAnchorInfoClass.getPrimitiveMethod(
@@ -105,6 +111,14 @@
         return FakeHolder.sInstance;
     }
 
+    public int getSelectionStart() {
+        return sGetSelectionStartMethod.invoke(mInstance);
+    }
+
+    public int getSelectionEnd() {
+        return sGetSelectionEndMethod.invoke(mInstance);
+    }
+
     public CharSequence getComposingText() {
         return sGetComposingTextMethod.invoke(mInstance);
     }
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 99a34be..863a8b7 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -98,6 +98,16 @@
     private final int mWidth;
     /** Height of the key, excluding the gap */
     private final int mHeight;
+    /**
+     * The combined width in pixels of the horizontal gaps belonging to this key, both to the left
+     * and to the right. I.e., mWidth + mHorizontalGap = total width belonging to the key.
+     */
+    private final int mHorizontalGap;
+    /**
+     * The combined height in pixels of the vertical gaps belonging to this key, both above and
+     * below. I.e., mHeight + mVerticalGap = total height belonging to the key.
+     */
+    private final int mVerticalGap;
     /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */
     private final int mX;
     /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */
@@ -198,8 +208,10 @@
             final String hintLabel, final int labelFlags, final int backgroundType, final int x,
             final int y, final int width, final int height, final int horizontalGap,
             final int verticalGap) {
-        mHeight = height - verticalGap;
         mWidth = width - horizontalGap;
+        mHeight = height - verticalGap;
+        mHorizontalGap = horizontalGap;
+        mVerticalGap = verticalGap;
         mHintLabel = hintLabel;
         mLabelFlags = labelFlags;
         mBackgroundType = backgroundType;
@@ -214,7 +226,7 @@
         mEnabled = (code != CODE_UNSPECIFIED);
         mIconId = iconId;
         // Horizontal gap is divided equally to both sides of the key.
-        mX = x + horizontalGap / 2;
+        mX = x + mHorizontalGap / 2;
         mY = y;
         mHitBox.set(x, y, x + width + 1, y + height);
         mKeyVisualAttributes = null;
@@ -235,18 +247,21 @@
      */
     public Key(final String keySpec, final TypedArray keyAttr, final KeyStyle style,
             final KeyboardParams params, final KeyboardRow row) {
-        final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
+        mHorizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
+        mVerticalGap = params.mVerticalGap;
+
+        final float horizontalGapFloat = mHorizontalGap;
         final int rowHeight = row.getRowHeight();
-        mHeight = rowHeight - params.mVerticalGap;
+        mHeight = rowHeight - mVerticalGap;
 
         final float keyXPos = row.getKeyX(keyAttr);
         final float keyWidth = row.getKeyWidth(keyAttr, keyXPos);
         final int keyYPos = row.getKeyY();
 
         // Horizontal gap is divided equally to both sides of the key.
-        mX = Math.round(keyXPos + horizontalGap / 2);
+        mX = Math.round(keyXPos + horizontalGapFloat / 2);
         mY = keyYPos;
-        mWidth = Math.round(keyWidth - horizontalGap);
+        mWidth = Math.round(keyWidth - horizontalGapFloat);
         mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1,
                 keyYPos + rowHeight);
         // Update row to have current x coordinate.
@@ -388,6 +403,8 @@
         mIconId = key.mIconId;
         mWidth = key.mWidth;
         mHeight = key.mHeight;
+        mHorizontalGap = key.mHorizontalGap;
+        mVerticalGap = key.mVerticalGap;
         mX = key.mX;
         mY = key.mY;
         mHitBox.set(key.mHitBox);
@@ -788,6 +805,24 @@
     }
 
     /**
+     * The combined width in pixels of the horizontal gaps belonging to this key, both above and
+     * below. I.e., getWidth() + getHorizontalGap() = total width belonging to the key.
+     * @return Horizontal gap belonging to this key.
+     */
+    public int getHorizontalGap() {
+        return mHorizontalGap;
+    }
+
+    /**
+     * The combined height in pixels of the vertical gaps belonging to this key, both above and
+     * below. I.e., getHeight() + getVerticalGap() = total height belonging to the key.
+     * @return Vertical gap belonging to this key.
+     */
+    public int getVerticalGap() {
+        return mVerticalGap;
+    }
+
+    /**
      * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap.
      * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap.
      */
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 43c6144..f9cf353 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -77,8 +77,7 @@
 
     private final int mHashCode;
 
-    public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params,
-        boolean isSplitLayout) {
+    public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) {
         mSubtype = params.mSubtype;
         mLocale = SubtypeLocaleUtils.getSubtypeLocale(mSubtype);
         mWidth = params.mKeyboardWidth;
@@ -91,7 +90,7 @@
         mCustomActionLabel = (mEditorInfo.actionLabel != null)
                 ? mEditorInfo.actionLabel.toString() : null;
         mHasShortcutKey = params.mVoiceInputKeyEnabled;
-        mIsSplitLayout = isSplitLayout;
+        mIsSplitLayout = params.mIsSplitLayoutEnabled;
 
         mHashCode = computeHashCode(this);
     }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index 1dbecdc..47fb7b3 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -115,6 +115,12 @@
         int mKeyboardWidth;
         int mKeyboardHeight;
         int mScriptId = ScriptUtils.SCRIPT_LATIN;
+        // Indicates if the user has enabled the split-layout preference
+        // and the required ProductionFlags are enabled.
+        boolean mIsSplitLayoutEnabledByUser;
+        // Indicates if split layout is actually enabled, taking into account
+        // whether the user has enabled it, and the keyboard layout supports it.
+        boolean mIsSplitLayoutEnabled;
         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
                 new SparseArray<>();
@@ -170,9 +176,9 @@
         // specified as an elementKeyboard attribute in the file.
         // The KeyboardId is an internal key for a Keyboard object.
 
-        // TODO: AND mSupportsSplitLayout with the user preference that forces a split.
-        final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams,
-                elementParams.mSupportsSplitLayout);
+        mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser
+                && elementParams.mSupportsSplitLayout;
+        final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams);
         try {
             return getKeyboard(elementParams, id);
         } catch (final RuntimeException e) {
@@ -290,12 +296,19 @@
             return this;
         }
 
-        public void disableTouchPositionCorrectionData() {
+        public Builder disableTouchPositionCorrectionData() {
             mParams.mDisableTouchPositionCorrectionDataForTest = true;
+            return this;
         }
 
-        public void setScriptId(final int scriptId) {
+        public Builder setScriptId(final int scriptId) {
             mParams.mScriptId = scriptId;
+            return this;
+        }
+
+        public Builder setSplitLayoutEnabledByUser(final boolean enabled) {
+            mParams.mIsSplitLayoutEnabledByUser = enabled;
+            return this;
         }
 
         public KeyboardLayoutSet build() {
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 6cd7955..246d11b 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -36,6 +36,7 @@
 import com.android.inputmethod.latin.RichInputMethodManager;
 import com.android.inputmethod.latin.SubtypeSwitcher;
 import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.define.ProductionFlags;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.settings.SettingsValues;
 import com.android.inputmethod.latin.utils.ResourceUtils;
@@ -114,6 +115,8 @@
         builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
         builder.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey);
         builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey());
+        builder.setSplitLayoutEnabledByUser(ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED
+                && settingsValues.mIsSplitKeyboardEnabled);
         mKeyboardLayoutSet = builder.build();
         try {
             mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState);
diff --git a/java/src/com/android/inputmethod/keyboard/TextDecorator.java b/java/src/com/android/inputmethod/keyboard/TextDecorator.java
index f614b22..315d363 100644
--- a/java/src/com/android/inputmethod/keyboard/TextDecorator.java
+++ b/java/src/com/android/inputmethod/keyboard/TextDecorator.java
@@ -17,23 +17,22 @@
 package com.android.inputmethod.keyboard;
 
 import android.graphics.Matrix;
-import android.graphics.PointF;
 import android.graphics.RectF;
 import android.inputmethodservice.InputMethodService;
 import android.os.Message;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
+import android.view.inputmethod.CursorAnchorInfo;
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper;
 
 import javax.annotation.Nonnull;
 
 /**
- * A controller class of commit/add-to-dictionary indicator (a.k.a. TextDecorator). This class
+ * A controller class of the add-to-dictionary indicator (a.k.a. TextDecorator). This class
  * is designed to be independent of UI subsystems such as {@link View}. All the UI related
  * operations are delegated to {@link TextDecoratorUi} via {@link TextDecoratorUiOperator}.
  */
@@ -41,18 +40,22 @@
     private static final String TAG = TextDecorator.class.getSimpleName();
     private static final boolean DEBUG = false;
 
-    private static final int MODE_NONE = 0;
-    private static final int MODE_COMMIT = 1;
-    private static final int MODE_ADD_TO_DICTIONARY = 2;
+    private static final int INVALID_CURSOR_INDEX = -1;
 
-    private int mMode = MODE_NONE;
+    private static final int MODE_MONITOR = 0;
+    private static final int MODE_WAITING_CURSOR_INDEX = 1;
+    private static final int MODE_SHOWING_INDICATOR = 2;
 
-    private final PointF mLocalOrigin = new PointF();
-    private final RectF mRelativeIndicatorBounds = new RectF();
-    private final RectF mRelativeComposingTextBounds = new RectF();
+    private int mMode = MODE_MONITOR;
+
+    private String mLastComposingText = null;
+    private RectF mIndicatorBoundsForLastComposingText = new RectF();
+    private RectF mComposingTextBoundsForLastComposingText = new RectF();
 
     private boolean mIsFullScreenMode = false;
-    private SuggestedWordInfo mWaitingWord = null;
+    private String mWaitingWord = null;
+    private int mWaitingCursorStart = INVALID_CURSOR_INDEX;
+    private int mWaitingCursorEnd = INVALID_CURSOR_INDEX;
     private CursorAnchorInfoCompatWrapper mCursorAnchorInfoWrapper = null;
 
     @Nonnull
@@ -63,16 +66,10 @@
 
     public interface Listener {
         /**
-         * Called when the user clicks the composing text to commit.
-         * @param wordInfo the suggested word which the user clicked on.
+         * Called when the user clicks the indicator to add the word into the dictionary.
+         * @param word the word which the user clicked on.
          */
-        void onClickComposingTextToCommit(final SuggestedWordInfo wordInfo);
-
-        /**
-         * Called when the user clicks the composing text to add the word into the dictionary.
-         * @param wordInfo the suggested word which the user clicked on.
-         */
-        void onClickComposingTextToAddToDictionary(final SuggestedWordInfo wordInfo);
+        void onClickComposingTextToAddToDictionary(final String word);
     }
 
     public TextDecorator(final Listener listener) {
@@ -103,46 +100,19 @@
     }
 
     /**
-     * Shows the "Commit" indicator and associates it with the given suggested word.
+     * Shows the "Add to dictionary" indicator and associates it with associating the given word.
      *
-     * <p>The effect of {@link #showCommitIndicator(SuggestedWordInfo)} and
-     * {@link #showAddToDictionaryIndicator(SuggestedWordInfo)} are exclusive to each other. Call
-     * {@link #reset()} to hide the indicator.</p>
-     *
-     * @param wordInfo the suggested word which should be associated with the indicator. This object
-     * will be passed back in {@link Listener#onClickComposingTextToCommit(SuggestedWordInfo)}
+     * @param word the word which should be associated with the indicator. This object will be
+     * passed back in {@link Listener#onClickComposingTextToAddToDictionary(String)}.
+     * @param selectionStart the cursor index (inclusive) when the indicator should be displayed.
+     * @param selectionEnd the cursor index (exclusive) when the indicator should be displayed.
      */
-    public void showCommitIndicator(final SuggestedWordInfo wordInfo) {
-        if (mMode == MODE_COMMIT && wordInfo != null &&
-                TextUtils.equals(mWaitingWord.mWord, wordInfo.mWord)) {
-            // Skip layout for better performance.
-            return;
-        }
-        mWaitingWord = wordInfo;
-        mMode = MODE_COMMIT;
-        layoutLater();
-    }
-
-    /**
-     * Shows the "Add to dictionary" indicator and associates it with associating the given
-     * suggested word.
-     *
-     * <p>The effect of {@link #showCommitIndicator(SuggestedWordInfo)} and
-     * {@link #showAddToDictionaryIndicator(SuggestedWordInfo)} are exclusive to each other. Call
-     * {@link #reset()} to hide the indicator.</p>
-     *
-     * @param wordInfo the suggested word which should be associated with the indicator. This object
-     * will be passed back in
-     * {@link Listener#onClickComposingTextToAddToDictionary(SuggestedWordInfo)}.
-     */
-    public void showAddToDictionaryIndicator(final SuggestedWordInfo wordInfo) {
-        if (mMode == MODE_ADD_TO_DICTIONARY && wordInfo != null &&
-                TextUtils.equals(mWaitingWord.mWord, wordInfo.mWord)) {
-            // Skip layout for better performance.
-            return;
-        }
-        mWaitingWord = wordInfo;
-        mMode = MODE_ADD_TO_DICTIONARY;
+    public void showAddToDictionaryIndicator(final String word, final int selectionStart,
+            final int selectionEnd) {
+        mWaitingWord = word;
+        mWaitingCursorStart = selectionStart;
+        mWaitingCursorEnd = selectionEnd;
+        mMode = MODE_WAITING_CURSOR_INDEX;
         layoutLater();
         return;
     }
@@ -165,18 +135,19 @@
      */
     public void reset() {
         mWaitingWord = null;
-        mMode = MODE_NONE;
-        mLocalOrigin.set(0.0f, 0.0f);
-        mRelativeIndicatorBounds.set(0.0f, 0.0f, 0.0f, 0.0f);
-        mRelativeComposingTextBounds.set(0.0f, 0.0f, 0.0f, 0.0f);
+        mMode = MODE_MONITOR;
+        mWaitingCursorStart = INVALID_CURSOR_INDEX;
+        mWaitingCursorEnd = INVALID_CURSOR_INDEX;
         cancelLayoutInternalExpectedly("Resetting internal state.");
     }
 
     /**
-     * Must be called when the {@link InputMethodService#onUpdateCursorAnchorInfo()} is called.
+     * Must be called when the {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)}
+     * is called.
      *
      * <p>CAVEAT: Currently the input method author is responsible for ignoring
-     * {@link InputMethodService#onUpdateCursorAnchorInfo()} called in full screen mode.</p>
+     * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} called in full screen
+     * mode.</p>
      * @param info the compatibility wrapper object for the received {@link CursorAnchorInfo}.
      */
     public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) {
@@ -185,29 +156,6 @@
         layoutImmediately();
     }
 
-    /**
-     * Hides indicator if the new composing text doesn't match the expected one.
-     *
-     * <p>Calling this method is optional but recommended whenever the new composition is passed to
-     * the application. The motivation of this method is to reduce the UI latency. With this method,
-     * we can hide the indicator without waiting the arrival of the
-     * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} callback, assuming that
-     * the application accepts the new composing text without any modification. Even if this
-     * assumption is false, the indicator will be shown again when
-     * {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is actually received.
-     * </p>
-     *
-     * @param newComposingText the new composing text that is being passed to the application.
-     */
-    public void hideIndicatorIfNecessary(final CharSequence newComposingText) {
-        if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) {
-            return;
-        }
-        if (!TextUtils.equals(newComposingText, mWaitingWord.mWord)) {
-            mUiOperator.hideUi();
-        }
-    }
-
     private void cancelLayoutInternalUnexpectedly(final String message) {
         mUiOperator.hideUi();
         Log.d(TAG, message);
@@ -232,15 +180,6 @@
     }
 
     private void layoutMain() {
-        if (mMode != MODE_COMMIT && mMode != MODE_ADD_TO_DICTIONARY) {
-            if (mMode == MODE_NONE) {
-                cancelLayoutInternalExpectedly("Not ready for layouting.");
-            } else {
-                cancelLayoutInternalUnexpectedly("Unknown mMode=" + mMode);
-            }
-            return;
-        }
-
         final CursorAnchorInfoCompatWrapper info = mCursorAnchorInfoWrapper;
 
         if (info == null) {
@@ -254,104 +193,117 @@
         }
 
         final CharSequence composingText = info.getComposingText();
-        if (mMode == MODE_COMMIT) {
-            if (composingText == null) {
-                cancelLayoutInternalExpectedly("composingText is null.");
-                return;
-            }
+        if (!TextUtils.isEmpty(composingText)) {
             final int composingTextStart = info.getComposingTextStart();
             final int lastCharRectIndex = composingTextStart + composingText.length() - 1;
             final RectF lastCharRect = info.getCharacterBounds(lastCharRectIndex);
-            final int lastCharRectFlag = info.getCharacterBoundsFlags(lastCharRectIndex);
+            final int lastCharRectFlags = info.getCharacterBoundsFlags(lastCharRectIndex);
             final boolean hasInvisibleRegionInLastCharRect =
-                    (lastCharRectFlag & CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION)
+                    (lastCharRectFlags & CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION)
                             != 0;
             if (lastCharRect == null || matrix == null || hasInvisibleRegionInLastCharRect) {
                 mUiOperator.hideUi();
                 return;
             }
-            final RectF segmentStartCharRect = new RectF(lastCharRect);
-            for (int i = composingText.length() - 2; i >= 0; --i) {
-                final RectF charRect = info.getCharacterBounds(composingTextStart + i);
-                if (charRect == null) {
+
+            // Note that the following layout information is fragile, and must be invalidated
+            // even when surrounding text next to the composing text is changed because it can
+            // affect how the composing text is rendered.
+            // TODO: Investigate if we can change the input logic to make the target text
+            // composing state so that we can retrieve the character bounds reliably.
+            final String composingTextString = composingText.toString();
+            final float top = lastCharRect.top;
+            final float bottom = lastCharRect.bottom;
+            float left = lastCharRect.left;
+            float right = lastCharRect.right;
+            boolean useRtlLayout = false;
+            for (int i = composingText.length() - 1; i >= 0; --i) {
+                final int characterIndex = composingTextStart + i;
+                final RectF characterBounds = info.getCharacterBounds(characterIndex);
+                final int characterBoundsFlags = info.getCharacterBoundsFlags(characterIndex);
+                if (characterBounds == null) {
                     break;
                 }
-                if (charRect.top != segmentStartCharRect.top) {
+                if (characterBounds.top != top) {
                     break;
                 }
-                if (charRect.bottom != segmentStartCharRect.bottom) {
+                if (characterBounds.bottom != bottom) {
                     break;
                 }
-                segmentStartCharRect.set(charRect);
+                if ((characterBoundsFlags & CursorAnchorInfoCompatWrapper.FLAG_IS_RTL) != 0) {
+                    // This is for both RTL text and bi-directional text. RTL languages usually mix
+                    // RTL characters with LTR characters and in this case we should display the
+                    // indicator on the left, while in LTR languages that normally never happens.
+                    // TODO: Try to come up with a better algorithm.
+                    useRtlLayout = true;
+                }
+                left = Math.min(characterBounds.left, left);
+                right = Math.max(characterBounds.right, right);
             }
-
-            mLocalOrigin.set(lastCharRect.right, lastCharRect.top);
-            mRelativeIndicatorBounds.set(lastCharRect.right, lastCharRect.top,
-                    lastCharRect.right + lastCharRect.height(), lastCharRect.bottom);
-            mRelativeIndicatorBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y);
-
-            mRelativeIndicatorBounds.set(lastCharRect.right, lastCharRect.top,
-                    lastCharRect.right + lastCharRect.height(), lastCharRect.bottom);
-            mRelativeIndicatorBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y);
-
-            mRelativeComposingTextBounds.set(segmentStartCharRect.left, segmentStartCharRect.top,
-                    segmentStartCharRect.right, segmentStartCharRect.bottom);
-            mRelativeComposingTextBounds.offset(-mLocalOrigin.x, -mLocalOrigin.y);
-
-            if (mWaitingWord == null) {
-                cancelLayoutInternalExpectedly("mWaitingText is null.");
-                return;
+            mLastComposingText = composingTextString;
+            mComposingTextBoundsForLastComposingText.set(left, top, right, bottom);
+            // The height and width of the indicator is the same as the height of the composing
+            // text.
+            final float indicatorSize = bottom - top;
+            mIndicatorBoundsForLastComposingText.set(0.0f, 0.0f, indicatorSize, indicatorSize);
+            // The horizontal position of the indicator depends on the text direction.
+            final float indicatorTop = top;
+            final float indicatorLeft;
+            if (useRtlLayout) {
+                indicatorLeft = left - indicatorSize;
+            } else {
+                indicatorLeft = right;
             }
-            if (TextUtils.isEmpty(mWaitingWord.mWord)) {
-                cancelLayoutInternalExpectedly("mWaitingText.mWord is empty.");
-                return;
-            }
-            if (!TextUtils.equals(composingText, mWaitingWord.mWord)) {
-                // This is indeed an expected situation because of the asynchronous nature of
-                // input method framework in Android. Note that composingText is notified from the
-                // application, while mWaitingWord.mWord is obtained directly from the InputLogic.
-                cancelLayoutInternalExpectedly(
-                        "Composing text doesn't match the one we are waiting for.");
-                return;
-            }
-        } else {
-            if (!mIsFullScreenMode && !TextUtils.isEmpty(composingText)) {
-                // This is an unexpected case.
-                // TODO: Document this.
-                mUiOperator.hideUi();
-                return;
-            }
-            // In MODE_ADD_TO_DICTIONARY, we cannot retrieve the character position at all because
-            // of the lack of composing text. We will use the insertion marker position instead.
-            if ((info.getInsertionMarkerFlags() &
-                    CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION) != 0) {
-                mUiOperator.hideUi();
-                return;
-            }
-            final float insertionMarkerHolizontal = info.getInsertionMarkerHorizontal();
-            final float insertionMarkerTop = info.getInsertionMarkerTop();
-            mLocalOrigin.set(insertionMarkerHolizontal, insertionMarkerTop);
+            mIndicatorBoundsForLastComposingText.offset(indicatorLeft, indicatorTop);
         }
 
-        final RectF indicatorBounds = new RectF(mRelativeIndicatorBounds);
-        final RectF composingTextBounds = new RectF(mRelativeComposingTextBounds);
-        indicatorBounds.offset(mLocalOrigin.x, mLocalOrigin.y);
-        composingTextBounds.offset(mLocalOrigin.x, mLocalOrigin.y);
-        mUiOperator.layoutUi(mMode == MODE_COMMIT, matrix, indicatorBounds, composingTextBounds);
+        final int selectionStart = info.getSelectionStart();
+        final int selectionEnd = info.getSelectionEnd();
+        switch (mMode) {
+            case MODE_MONITOR:
+                mUiOperator.hideUi();
+                return;
+            case MODE_WAITING_CURSOR_INDEX:
+                if (selectionStart != mWaitingCursorStart || selectionEnd != mWaitingCursorEnd) {
+                    mUiOperator.hideUi();
+                    return;
+                }
+                mMode = MODE_SHOWING_INDICATOR;
+                break;
+            case MODE_SHOWING_INDICATOR:
+                if (selectionStart != mWaitingCursorStart || selectionEnd != mWaitingCursorEnd) {
+                    mUiOperator.hideUi();
+                    mMode = MODE_MONITOR;
+                    mWaitingCursorStart = INVALID_CURSOR_INDEX;
+                    mWaitingCursorEnd = INVALID_CURSOR_INDEX;
+                    return;
+                }
+                break;
+            default:
+                cancelLayoutInternalUnexpectedly("Unexpected internal mode=" + mMode);
+                return;
+        }
+
+        if (!TextUtils.equals(mLastComposingText, mWaitingWord)) {
+            cancelLayoutInternalUnexpectedly("mLastComposingText doesn't match mWaitingWord");
+            return;
+        }
+
+        if ((info.getInsertionMarkerFlags() &
+                CursorAnchorInfoCompatWrapper.FLAG_HAS_INVISIBLE_REGION) != 0) {
+            mUiOperator.hideUi();
+            return;
+        }
+
+        mUiOperator.layoutUi(matrix, mIndicatorBoundsForLastComposingText,
+                mComposingTextBoundsForLastComposingText);
     }
 
     private void onClickIndicator() {
-        if (mWaitingWord == null || TextUtils.isEmpty(mWaitingWord.mWord)) {
+        if (mMode != MODE_SHOWING_INDICATOR) {
             return;
         }
-        switch (mMode) {
-            case MODE_COMMIT:
-                mListener.onClickComposingTextToCommit(mWaitingWord);
-                break;
-            case MODE_ADD_TO_DICTIONARY:
-                mListener.onClickComposingTextToAddToDictionary(mWaitingWord);
-                break;
-        }
+        mListener.onClickComposingTextToAddToDictionary(mWaitingWord);
     }
 
     private final LayoutInvalidator mLayoutInvalidator = new LayoutInvalidator(this);
@@ -407,10 +359,7 @@
 
     private final static Listener EMPTY_LISTENER = new Listener() {
         @Override
-        public void onClickComposingTextToCommit(SuggestedWordInfo wordInfo) {
-        }
-        @Override
-        public void onClickComposingTextToAddToDictionary(SuggestedWordInfo wordInfo) {
+        public void onClickComposingTextToAddToDictionary(final String word) {
         }
     };
 
@@ -425,8 +374,7 @@
         public void setOnClickListener(Runnable listener) {
         }
         @Override
-        public void layoutUi(boolean isCommitMode, Matrix matrix, RectF indicatorBounds,
-                RectF composingTextBounds) {
+        public void layoutUi(Matrix matrix, RectF indicatorBounds, RectF composingTextBounds) {
         }
     };
 }
diff --git a/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java b/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java
index 6e215a9..b67d177 100644
--- a/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java
+++ b/java/src/com/android/inputmethod/keyboard/TextDecoratorUi.java
@@ -46,7 +46,6 @@
     private static final int VISUAL_DEBUG_HIT_AREA_COLOR = 0x80ff8000;
 
     private final RelativeLayout mLocalRootView;
-    private final CommitIndicatorView mCommitIndicatorView;
     private final AddToDictionaryIndicatorView mAddToDictionaryIndicatorView;
     private final PopupWindow mTouchEventWindow;
     private final View mTouchEventWindowClickListenerView;
@@ -73,9 +72,7 @@
         mLocalRootView.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
 
         final ViewGroup contentView = getContentView(inputView);
-        mCommitIndicatorView = new CommitIndicatorView(context);
         mAddToDictionaryIndicatorView = new AddToDictionaryIndicatorView(context);
-        mLocalRootView.addView(mCommitIndicatorView);
         mLocalRootView.addView(mAddToDictionaryIndicatorView);
         if (contentView != null) {
             contentView.addView(mLocalRootView);
@@ -110,17 +107,15 @@
 
     @Override
     public void hideUi() {
-        mCommitIndicatorView.setVisibility(View.GONE);
         mAddToDictionaryIndicatorView.setVisibility(View.GONE);
         mTouchEventWindow.dismiss();
     }
 
     @Override
-    public void layoutUi(final boolean isCommitMode, final Matrix matrix,
-            final RectF indicatorBounds, final RectF composingTextBounds) {
+    public void layoutUi(final Matrix matrix, final RectF indicatorBounds,
+            final RectF composingTextBounds) {
         final RectF indicatorBoundsInScreenCoordinates = new RectF();
         matrix.mapRect(indicatorBoundsInScreenCoordinates, indicatorBounds);
-        mCommitIndicatorView.setBounds(indicatorBoundsInScreenCoordinates);
         mAddToDictionaryIndicatorView.setBounds(indicatorBoundsInScreenCoordinates);
 
         final RectF hitAreaBounds = new RectF(composingTextBounds);
@@ -133,20 +128,9 @@
         mLocalRootView.getLocationOnScreen(originScreen);
         final int viewOriginX = originScreen[0];
         final int viewOriginY = originScreen[1];
-
-        final View toBeShown;
-        final View toBeHidden;
-        if (isCommitMode) {
-            toBeShown = mCommitIndicatorView;
-            toBeHidden = mAddToDictionaryIndicatorView;
-        } else {
-            toBeShown = mAddToDictionaryIndicatorView;
-            toBeHidden = mCommitIndicatorView;
-        }
-        toBeShown.setX(indicatorBoundsInScreenCoordinates.left - viewOriginX);
-        toBeShown.setY(indicatorBoundsInScreenCoordinates.top - viewOriginY);
-        toBeShown.setVisibility(View.VISIBLE);
-        toBeHidden.setVisibility(View.GONE);
+        mAddToDictionaryIndicatorView.setX(indicatorBoundsInScreenCoordinates.left - viewOriginX);
+        mAddToDictionaryIndicatorView.setY(indicatorBoundsInScreenCoordinates.top - viewOriginY);
+        mAddToDictionaryIndicatorView.setVisibility(View.VISIBLE);
 
         if (mTouchEventWindow.isShowing()) {
             mTouchEventWindow.update((int)hitAreaBoundsInScreenCoordinates.left - viewOriginX,
@@ -239,15 +223,6 @@
         return windowContentView;
     }
 
-    private static final class CommitIndicatorView extends TextDecoratorUi.IndicatorView {
-        public CommitIndicatorView(final Context context) {
-            super(context, R.array.text_decorator_commit_indicator_path,
-                    R.integer.text_decorator_commit_indicator_path_size,
-                    R.color.text_decorator_commit_indicator_background_color,
-                    R.color.text_decorator_commit_indicator_foreground_color);
-        }
-    }
-
     private static final class AddToDictionaryIndicatorView extends TextDecoratorUi.IndicatorView {
         public AddToDictionaryIndicatorView(final Context context) {
             super(context, R.array.text_decorator_add_to_dictionary_indicator_path,
diff --git a/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java b/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java
index f84e12d..9c0b64a 100644
--- a/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java
+++ b/java/src/com/android/inputmethod/keyboard/TextDecoratorUiOperator.java
@@ -44,12 +44,10 @@
 
     /**
      * Called when the layout should be updated.
-     * @param isCommitMode {@code true} if the commit indicator should be shown. Show the
-     * add-to-dictionary indicator otherwise.
      * @param matrix The matrix that transforms the local coordinates into the screen coordinates.
      * @param indicatorBounds The bounding box of the indicator, in local coordinates.
      * @param composingTextBounds The bounding box of the composing text, in local coordinates.
      */
-    void layoutUi(final boolean isCommitMode, final Matrix matrix, final RectF indicatorBounds,
+    void layoutUi(final Matrix matrix, final RectF indicatorBounds,
             final RectF composingTextBounds);
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index fa41927..2056a0b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -674,15 +674,18 @@
                     R.styleable.Keyboard_Case_languageCode, id.mLocale.getLanguage());
             final boolean countryCodeMatched = matchString(caseAttr,
                     R.styleable.Keyboard_Case_countryCode, id.mLocale.getCountry());
+            final boolean splitLayoutMatched = matchBoolean(caseAttr,
+                    R.styleable.Keyboard_Case_isSplitLayout, id.mIsSplitLayout);
             final boolean selected = keyboardLayoutSetMatched && keyboardLayoutSetElementMatched
                     && keyboardThemeMacthed && modeMatched && navigateNextMatched
                     && navigatePreviousMatched && passwordInputMatched && clobberSettingsKeyMatched
                     && hasShortcutKeyMatched  && languageSwitchKeyEnabledMatched
                     && isMultiLineMatched && imeActionMatched && isIconDefinedMatched
-                    && localeCodeMatched && languageCodeMatched && countryCodeMatched;
+                    && localeCodeMatched && languageCodeMatched && countryCodeMatched
+                    && splitLayoutMatched;
 
             if (DEBUG) {
-                startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
+                startTag("<%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s>%s", TAG_CASE,
                         textAttr(caseAttr.getString(
                                 R.styleable.Keyboard_Case_keyboardLayoutSet), "keyboardLayoutSet"),
                         textAttr(caseAttr.getString(
@@ -707,6 +710,8 @@
                                 "languageSwitchKeyEnabled"),
                         booleanAttr(caseAttr, R.styleable.Keyboard_Case_isMultiLine,
                                 "isMultiLine"),
+                        booleanAttr(caseAttr, R.styleable.Keyboard_Case_isSplitLayout,
+                                "splitLayout"),
                         textAttr(caseAttr.getString(R.styleable.Keyboard_Case_isIconDefined),
                                 "isIconDefined"),
                         textAttr(caseAttr.getString(R.styleable.Keyboard_Case_localeCode),
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index eced45e..0f09daf 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -595,8 +595,9 @@
             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
             final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) {
         final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
-        final SuggestionResults suggestionResults =
-                new SuggestionResults(SuggestedWords.MAX_SUGGESTIONS);
+        final SuggestionResults suggestionResults = new SuggestionResults(
+                SuggestedWords.MAX_SUGGESTIONS,
+                prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence);
         final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT };
         for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
             for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 4757820..69fe6de 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -189,9 +189,8 @@
         private static final int MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED = 6;
         private static final int MSG_RESET_CACHES = 7;
         private static final int MSG_WAIT_FOR_DICTIONARY_LOAD = 8;
-        private static final int MSG_SHOW_COMMIT_INDICATOR = 9;
         // Update this when adding new messages
-        private static final int MSG_LAST = MSG_SHOW_COMMIT_INDICATOR;
+        private static final int MSG_LAST = MSG_WAIT_FOR_DICTIONARY_LOAD;
 
         private static final int ARG1_NOT_GESTURE_INPUT = 0;
         private static final int ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT = 1;
@@ -202,7 +201,6 @@
 
         private int mDelayInMillisecondsToUpdateSuggestions;
         private int mDelayInMillisecondsToUpdateShiftState;
-        private int mDelayInMillisecondsToShowCommitIndicator;
 
         public UIHandler(final LatinIME ownerInstance) {
             super(ownerInstance);
@@ -218,8 +216,6 @@
                     R.integer.config_delay_in_milliseconds_to_update_suggestions);
             mDelayInMillisecondsToUpdateShiftState = res.getInteger(
                     R.integer.config_delay_in_milliseconds_to_update_shift_state);
-            mDelayInMillisecondsToShowCommitIndicator = res.getInteger(
-                    R.integer.text_decorator_delay_in_milliseconds_to_show_commit_indicator);
         }
 
         @Override
@@ -277,14 +273,6 @@
                             latinIme.getCurrentRecapitalizeState());
                 }
                 break;
-            case MSG_SHOW_COMMIT_INDICATOR:
-                // Protocol of MSG_SET_COMMIT_INDICATOR_ENABLED:
-                // - what: MSG_SHOW_COMMIT_INDICATOR
-                // - arg1: not used.
-                // - arg2: not used.
-                // - obj:  the Runnable object to be called back.
-                ((Runnable) msg.obj).run();
-                break;
             case MSG_WAIT_FOR_DICTIONARY_LOAD:
                 Log.i(TAG, "Timeout waiting for dictionary load");
                 break;
@@ -385,19 +373,6 @@
             obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget();
         }
 
-        /**
-         * Posts a delayed task to show the commit indicator.
-         *
-         * <p>Only one task can exist in the queue. When this method is called, any prior task that
-         * has not yet fired will be canceled.</p>
-         * @param task the runnable object that will be fired when the delayed task is dispatched.
-         */
-        public void postShowCommitIndicatorTask(final Runnable task) {
-            removeMessages(MSG_SHOW_COMMIT_INDICATOR);
-            sendMessageDelayed(obtainMessage(MSG_SHOW_COMMIT_INDICATOR, task),
-                    mDelayInMillisecondsToShowCommitIndicator);
-        }
-
         // Working variables for the following methods.
         private boolean mIsOrientationChanging;
         private boolean mPendingSuccessiveImsCallback;
@@ -1516,19 +1491,23 @@
         final boolean isEmptyApplicationSpecifiedCompletions =
                 currentSettingsValues.isApplicationSpecifiedCompletionsOn()
                 && suggestedWords.isEmpty();
-        final boolean noSuggestionsToShow = (SuggestedWords.EMPTY == suggestedWords)
+        final boolean noSuggestionsFromDictionaries = (SuggestedWords.EMPTY == suggestedWords)
                 || suggestedWords.isPunctuationSuggestions()
                 || isEmptyApplicationSpecifiedCompletions;
-        if (shouldShowImportantNotice && noSuggestionsToShow) {
+        final boolean isBeginningOfSentencePrediction = (suggestedWords.mInputStyle
+                == SuggestedWords.INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION);
+        final boolean noSuggestionsToOverrideImportantNotice = noSuggestionsFromDictionaries
+                || isBeginningOfSentencePrediction;
+        if (shouldShowImportantNotice && noSuggestionsToOverrideImportantNotice) {
             if (mSuggestionStripView.maybeShowImportantNoticeTitle()) {
                 return;
             }
         }
 
         if (currentSettingsValues.isSuggestionsEnabledPerUserSettings()
-                // We should clear suggestions if there is no suggestion to show.
-                || noSuggestionsToShow
-                || currentSettingsValues.isApplicationSpecifiedCompletionsOn()) {
+                || currentSettingsValues.isApplicationSpecifiedCompletionsOn()
+                // We should clear the contextual strip if there is no suggestion from dictionaries.
+                || noSuggestionsFromDictionaries) {
             mSuggestionStripView.setSuggestions(suggestedWords,
                     SubtypeLocaleUtils.isRtlLanguage(mSubtypeSwitcher.getCurrentSubtype()));
         }
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index dc00ecc..d672430 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -252,7 +252,7 @@
      * See {@link InputConnection#commitText(CharSequence, int)}.
      */
     public void commitText(final CharSequence text, final int newCursorPosition) {
-        commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT);
+        commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT, text.length());
     }
 
     /**
@@ -265,9 +265,11 @@
      * 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.
+     * @param coloredTextLength the length of text, in Java chars, which should be rendered with
+     * the given background color.
      */
     public void commitTextWithBackgroundColor(final CharSequence text, final int newCursorPosition,
-            final int color) {
+            final int color, final int coloredTextLength) {
         if (DEBUG_BATCH_NESTING) checkBatchEdit();
         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
         mCommittedTextBeforeComposingText.append(text);
@@ -285,7 +287,8 @@
                 mTempObjectForCommitText.clear();
                 mTempObjectForCommitText.append(text);
                 final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(color);
-                mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, text.length(),
+                final int spanLength = Math.min(coloredTextLength, text.length());
+                mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, spanLength,
                         Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                 mIC.commitText(mTempObjectForCommitText, newCursorPosition);
                 mLastCommittedTextHasBackgroundColor = true;
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 4ad5ba6..1ecc995 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -188,8 +188,14 @@
             suggestionsList = suggestionsContainer;
         }
 
-        final int inputStyle = resultsArePredictions ? SuggestedWords.INPUT_STYLE_PREDICTION :
-                inputStyleIfNotPrediction;
+        final int inputStyle;
+        if (resultsArePredictions) {
+            inputStyle = suggestionResults.mIsBeginningOfSentence
+                    ? SuggestedWords.INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION
+                    : SuggestedWords.INPUT_STYLE_PREDICTION;
+        } else {
+            inputStyle = inputStyleIfNotPrediction;
+        }
         callback.onGetSuggestedWords(new SuggestedWords(suggestionsList,
                 suggestionResults.mRawSuggestions,
                 // TODO: this first argument is lying. If this is a whitelisted word which is an
@@ -244,6 +250,8 @@
 
         // In the batch input mode, the most relevant suggested word should act as a "typed word"
         // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
+        // Note that because this method is never used to get predictions, there is no need to
+        // modify inputType such in getSuggestedWordsForNonBatchInput.
         callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer,
                 suggestionResults.mRawSuggestions,
                 true /* typedWordValid */,
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index dcfaa3f..3eefafc 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -39,6 +39,7 @@
     public static final int INPUT_STYLE_APPLICATION_SPECIFIED = 4;
     public static final int INPUT_STYLE_RECORRECTION = 5;
     public static final int INPUT_STYLE_PREDICTION = 6;
+    public static final int INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION = 7;
 
     // The maximum number of suggestions available.
     public static final int MAX_SUGGESTIONS = 18;
@@ -80,10 +81,9 @@
             final int inputStyle,
             final int sequenceNumber) {
         this(suggestedWordInfoList, rawSuggestions,
-                (suggestedWordInfoList.isEmpty() || INPUT_STYLE_PREDICTION == inputStyle) ? null
+                (suggestedWordInfoList.isEmpty() || isPrediction(inputStyle)) ? null
                         : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord,
-                typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle,
-                sequenceNumber);
+                typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, sequenceNumber);
     }
 
     public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
@@ -180,6 +180,7 @@
         return "SuggestedWords:"
                 + " mTypedWordValid=" + mTypedWordValid
                 + " mWillAutoCorrect=" + mWillAutoCorrect
+                + " mInputStyle=" + mInputStyle
                 + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray());
     }
 
@@ -386,8 +387,13 @@
         }
     }
 
+    private static boolean isPrediction(final int inputStyle) {
+        return INPUT_STYLE_PREDICTION == inputStyle
+                || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle;
+    }
+
     public boolean isPrediction() {
-        return INPUT_STYLE_PREDICTION == mInputStyle;
+        return isPrediction(mInputStyle);
     }
 
     // SuggestedWords is an immutable object, as much as possible. We must not just remove
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 0942c07..8eccd5c 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -28,6 +28,7 @@
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
 import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
 
 import com.android.inputmethod.compat.CursorAnchorInfoCompatWrapper;
@@ -91,12 +92,8 @@
 
     private final TextDecorator mTextDecorator = new TextDecorator(new TextDecorator.Listener() {
         @Override
-        public void onClickComposingTextToCommit(SuggestedWordInfo wordInfo) {
-            mLatinIME.pickSuggestionManually(wordInfo);
-        }
-        @Override
-        public void onClickComposingTextToAddToDictionary(SuggestedWordInfo wordInfo) {
-            mLatinIME.addWordToUserDictionary(wordInfo.mWord);
+        public void onClickComposingTextToAddToDictionary(final String word) {
+            mLatinIME.addWordToUserDictionary(word);
             mLatinIME.dismissAddToDictionaryHint();
         }
     });
@@ -171,6 +168,7 @@
                 mConnection.requestCursorUpdates(true /* enableMonitor */,
                         true /* requestImmediateCallback */);
             }
+            mTextDecorator.reset();
         }
     }
 
@@ -207,6 +205,8 @@
     public void finishInput() {
         if (mWordComposer.isComposingWord()) {
             mConnection.finishComposingText();
+            StatsUtils.onWordCommitUserTyped(
+                    mWordComposer.getTypedWord(), mWordComposer.isBatchMode());
         }
         resetComposingState(true /* alsoResetLastComposedWord */);
         mInputLogicHandler.reset();
@@ -253,6 +253,7 @@
             promotePhantomSpace(settingsValues);
         }
         mConnection.commitText(text, 1);
+        StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode());
         mConnection.endBatchEdit();
         // Space state must be updated before calling updateShiftState
         mSpaceState = SpaceState.NONE;
@@ -334,17 +335,8 @@
         }
 
         final boolean shouldShowAddToDictionaryHint = shouldShowAddToDictionaryHint(suggestionInfo);
-        final boolean shouldShowAddToDictionaryIndicator =
-                shouldShowAddToDictionaryHint && settingsValues.mShouldShowUiToAcceptTypedWord;
-        final int backgroundColor;
-        if (shouldShowAddToDictionaryIndicator) {
-            backgroundColor = settingsValues.mTextHighlightColorForAddToDictionaryIndicator;
-        } else {
-            backgroundColor = Color.TRANSPARENT;
-        }
-        commitChosenWordWithBackgroundColor(settingsValues, suggestion,
-                LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR,
-                backgroundColor);
+        commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
+                LastComposedWord.NOT_A_SEPARATOR);
         mConnection.endBatchEdit();
         // Don't allow cancellation of manual pick
         mLastComposedWord.deactivate();
@@ -359,11 +351,10 @@
             // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE.
             handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE);
         }
-        if (shouldShowAddToDictionaryIndicator) {
-            mTextDecorator.showAddToDictionaryIndicator(suggestionInfo);
-        }
 
         StatsUtils.onPickSuggestionManually(mSuggestedWords, suggestionInfo);
+        StatsUtils.onWordCommitSuggestionPickedManually(
+                suggestionInfo.mWord, mWordComposer.isBatchMode());
         return inputTransaction;
     }
 
@@ -433,6 +424,9 @@
         mRecapitalizeStatus.enable();
         // We moved the cursor and need to invalidate the indicator right now.
         mTextDecorator.reset();
+        // Remaining background color that was used for the add-to-dictionary indicator should be
+        // removed.
+        mConnection.removeBackgroundColorFromHighlightedTextIfNecessary();
         // We moved the cursor. If we are touching a word, we need to resume suggestion.
         mLatinIME.mHandler.postResumeSuggestions(false /* shouldIncludeResumedWordInSuggestions */,
                 true /* shouldDelay */);
@@ -511,7 +505,9 @@
         handler.cancelUpdateSuggestionStrip();
         ++mAutoCommitSequenceNumber;
         mConnection.beginBatchEdit();
-        if (mWordComposer.isComposingWord()) {
+        if (!mWordComposer.isComposingWord()) {
+            mConnection.removeBackgroundColorFromHighlightedTextIfNecessary();
+        } else {
             if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
                 // If we are in the middle of a recorrection, we need to commit the recorrection
                 // first so that we can insert the batch input at the current cursor position.
@@ -582,6 +578,7 @@
                     batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
                     promotePhantomSpace(settingsValues);
                     mConnection.commitText(commitParts[0], 0);
+                    StatsUtils.onWordCommitUserTyped(commitParts[0], mWordComposer.isBatchMode());
                     mSpaceState = SpaceState.PHANTOM;
                     keyboardSwitcher.requestUpdatingShiftState(
                             getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState());
@@ -630,42 +627,6 @@
         }
         mSuggestedWords = suggestedWords;
         final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect;
-        if (shouldShowCommitIndicator(suggestedWords, settingsValues)) {
-            // typedWordInfo is never null here.
-            final int textBackgroundColor = settingsValues.mTextHighlightColorForCommitIndicator;
-            final SuggestedWordInfo typedWordInfo = suggestedWords.getTypedWordInfoOrNull();
-            handler.postShowCommitIndicatorTask(new Runnable() {
-                @Override
-                public void run() {
-                    // TODO: This needs to be refactored to ensure that mWordComposer is accessed
-                    // only from the UI thread.
-                    if (!mWordComposer.isComposingWord()) {
-                        mTextDecorator.reset();
-                        return;
-                    }
-                    final SuggestedWordInfo currentTypedWordInfo =
-                            mSuggestedWords.getTypedWordInfoOrNull();
-                    if (currentTypedWordInfo == null) {
-                        mTextDecorator.reset();
-                        return;
-                    }
-                    if (!currentTypedWordInfo.equals(typedWordInfo)) {
-                        // Suggested word has been changed. This task is obsolete.
-                        mTextDecorator.reset();
-                        return;
-                    }
-                    // TODO: As with the above TODO comment, this operation must be performed only
-                    // on the UI thread too. Needs to be refactored.
-                    setComposingTextInternalWithBackgroundColor(typedWordInfo.mWord,
-                            1 /* newCursorPosition */, textBackgroundColor);
-                    mTextDecorator.showCommitIndicator(typedWordInfo);
-                }
-            });
-        } else {
-            // Note: It is OK to not cancel previous postShowCommitIndicatorTask() here. Having a
-            // cancellation mechanism could improve performance a bit though.
-            mTextDecorator.reset();
-        }
 
         // Put a blue underline to a word in TextView which will be auto-corrected.
         if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
@@ -843,13 +804,14 @@
             final InputTransaction inputTransaction,
             // TODO: remove this argument
             final LatinIME.UIHandler handler) {
-        // In case the "add to dictionary" hint was still displayed.
-        // TODO: Do we really need to check if we have composing text here?
-        if (!mWordComposer.isComposingWord() &&
-                mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) {
-            mSuggestionStripViewAccessor.dismissAddToDictionaryHint();
+        if (!mWordComposer.isComposingWord()) {
             mConnection.removeBackgroundColorFromHighlightedTextIfNecessary();
-            mTextDecorator.reset();
+            // In case the "add to dictionary" hint was still displayed.
+            // TODO: Do we really need to check if we have composing text here?
+            if (mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) {
+                mSuggestionStripViewAccessor.dismissAddToDictionaryHint();
+                mTextDecorator.reset();
+            }
         }
 
         final int codePoint = event.mCodePoint;
@@ -1108,8 +1070,10 @@
             inputTransaction.setRequiresUpdateSuggestions();
         } else {
             if (mLastComposedWord.canRevertCommit()) {
-                revertCommit(inputTransaction);
+                final String lastComposedWord = mLastComposedWord.mTypedWord;
+                revertCommit(inputTransaction, inputTransaction.mSettingsValues);
                 StatsUtils.onRevertAutoCorrect();
+                StatsUtils.onWordCommitUserTyped(lastComposedWord, mWordComposer.isBatchMode());
                 return;
             }
             if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
@@ -1609,14 +1573,19 @@
      * This is triggered upon pressing backspace just after a commit with auto-correction.
      *
      * @param inputTransaction The transaction in progress.
+     * @param settingsValues the current values of the settings.
      */
-    private void revertCommit(final InputTransaction inputTransaction) {
+    private void revertCommit(final InputTransaction inputTransaction,
+            final SettingsValues settingsValues) {
         final CharSequence originallyTypedWord = mLastComposedWord.mTypedWord;
+        final String originallyTypedWordString =
+                originallyTypedWord != null ? originallyTypedWord.toString() : "";
         final CharSequence committedWord = mLastComposedWord.mCommittedWord;
         final String committedWordString = committedWord.toString();
         final int cancelLength = committedWord.length();
+        final String separatorString = mLastComposedWord.mSeparatorString;
         // We want java chars, not codepoints for the following.
-        final int separatorLength = mLastComposedWord.mSeparatorString.length();
+        final int separatorLength = separatorString.length();
         // TODO: should we check our saved separator against the actual contents of the text view?
         final int deleteLength = cancelLength + separatorLength;
         if (DebugFlags.DEBUG_ENABLED) {
@@ -1635,7 +1604,7 @@
         if (!TextUtils.isEmpty(committedWord)) {
             mDictionaryFacilitator.removeWordFromPersonalizedDicts(committedWordString);
         }
-        final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
+        final String stringToCommit = originallyTypedWord + separatorString;
         final SpannableString textToCommit = new SpannableString(stringToCommit);
         if (committedWord instanceof SpannableString) {
             final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord;
@@ -1672,23 +1641,53 @@
                     suggestions.toArray(new String[suggestions.size()]), 0 /* flags */),
                     0 /* start */, lastCharIndex /* end */, 0 /* flags */);
         }
+
+        final boolean shouldShowAddToDictionaryForTypedWord =
+                shouldShowAddToDictionaryForTypedWord(mLastComposedWord, settingsValues);
+
         if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) {
             // For languages with spaces, we revert to the typed string, but the cursor is still
             // after the separator so we don't resume suggestions. If the user wants to correct
             // the word, they have to press backspace again.
-            mConnection.commitText(textToCommit, 1);
+            if (shouldShowAddToDictionaryForTypedWord) {
+                mConnection.commitTextWithBackgroundColor(textToCommit, 1,
+                        settingsValues.mTextHighlightColorForAddToDictionaryIndicator,
+                        originallyTypedWordString.length());
+            } else {
+                mConnection.commitText(textToCommit, 1);
+            }
         } 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.
             final int[] codePoints = StringUtils.toCodePointArray(stringToCommit);
             mWordComposer.setComposingWord(codePoints,
                     mLatinIME.getCoordinatesForCurrentKeyboard(codePoints));
-            setComposingTextInternal(textToCommit, 1);
+            if (shouldShowAddToDictionaryForTypedWord) {
+                setComposingTextInternalWithBackgroundColor(textToCommit, 1,
+                        settingsValues.mTextHighlightColorForAddToDictionaryIndicator,
+                        originallyTypedWordString.length());
+            } else {
+                setComposingTextInternal(textToCommit, 1);
+            }
         }
         // Don't restart suggestion yet. We'll restart if the user deletes the separator.
         mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
-        // We have a separator between the word and the cursor: we should show predictions.
-        inputTransaction.setRequiresUpdateSuggestions();
+
+        if (shouldShowAddToDictionaryForTypedWord) {
+            // Due to the API limitation as of L, we cannot reliably retrieve the reverted text
+            // when the separator causes line breaking. Until this API limitation is addressed in
+            // the framework, show the indicator only when the separator doesn't contain
+            // line-breaking characters.
+            if (!StringUtils.hasLineBreakCharacter(separatorString)) {
+                mTextDecorator.showAddToDictionaryIndicator(originallyTypedWordString,
+                        mConnection.getExpectedSelectionStart(),
+                        mConnection.getExpectedSelectionEnd());
+            }
+            mSuggestionStripViewAccessor.showAddToDictionaryHint(originallyTypedWordString);
+        } else {
+            // We have a separator between the word and the cursor: we should show predictions.
+            inputTransaction.setRequiresUpdateSuggestions();
+        }
     }
 
     /**
@@ -2010,6 +2009,8 @@
             final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
             if (0 != indexOfLastSpace) {
                 mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
+                StatsUtils.onWordCommitUserTyped(
+                        batchInputText.substring(0, indexOfLastSpace), mWordComposer.isBatchMode());
                 final SuggestedWords suggestedWordsForLastWordOfPhraseGesture =
                         suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture();
                 mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture);
@@ -2048,8 +2049,10 @@
         if (!mWordComposer.isComposingWord()) return;
         final String typedWord = mWordComposer.getTypedWord();
         if (typedWord.length() > 0) {
+            final boolean isBatchMode = mWordComposer.isBatchMode();
             commitChosenWord(settingsValues, typedWord,
                     LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString);
+            StatsUtils.onWordCommitUserTyped(typedWord, isBatchMode);
         }
     }
 
@@ -2095,6 +2098,7 @@
                 throw new RuntimeException("We have an auto-correction but the typed word "
                         + "is empty? Impossible! I must commit suicide.");
             }
+            final boolean isBatchMode = mWordComposer.isBatchMode();
             commitChosenWord(settingsValues, autoCorrection,
                     LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator);
             if (!typedWord.equals(autoCorrection)) {
@@ -2107,41 +2111,25 @@
                 mConnection.commitCorrection(new CorrectionInfo(
                         mConnection.getExpectedSelectionEnd() - autoCorrection.length(),
                         typedWord, autoCorrection));
-                StatsUtils.onAutoCorrection(typedWord, autoCorrection, mWordComposer.isBatchMode(),
+                StatsUtils.onAutoCorrection(typedWord, autoCorrection, isBatchMode,
                         mWordComposer.getAutoCorrectionDictionaryTypeOrNull());
+                StatsUtils.onWordCommitAutoCorrect(autoCorrection, isBatchMode);
+            } else {
+                StatsUtils.onWordCommitUserTyped(autoCorrection, isBatchMode);
             }
         }
     }
 
     /**
-     * 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.
-     * @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.
-     */
-    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) {
+    private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
+            final int commitType, final String separatorString) {
         final SuggestedWords suggestedWords = mSuggestedWords;
         final CharSequence chosenWordWithSuggestions =
                 SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
@@ -2151,7 +2139,7 @@
         // information from the 1st previous word.
         final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord(
                 settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1);
-        mConnection.commitTextWithBackgroundColor(chosenWordWithSuggestions, 1, backgroundColor);
+        mConnection.commitText(chosenWordWithSuggestions, 1);
         // 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
@@ -2235,7 +2223,7 @@
     private void setComposingTextInternal(final CharSequence newComposingText,
             final int newCursorPosition) {
         setComposingTextInternalWithBackgroundColor(newComposingText, newCursorPosition,
-                Color.TRANSPARENT);
+                Color.TRANSPARENT, newComposingText.length());
     }
 
     /**
@@ -2251,9 +2239,11 @@
      * @param newCursorPosition the new cursor position
      * @param backgroundColor the background color to be set to the composing text. Set
      * {@link Color#TRANSPARENT} to disable the background color.
+     * @param coloredTextLength the length of text, in Java chars, which should be rendered with
+     * the given background color.
      */
     private void setComposingTextInternalWithBackgroundColor(final CharSequence newComposingText,
-            final int newCursorPosition, final int backgroundColor) {
+            final int newCursorPosition, final int backgroundColor, final int coloredTextLength) {
         final CharSequence composingTextToBeSet;
         if (backgroundColor == Color.TRANSPARENT) {
             composingTextToBeSet = newComposingText;
@@ -2261,7 +2251,8 @@
             final SpannableString spannable = new SpannableString(newComposingText);
             final BackgroundColorSpan backgroundColorSpan =
                     new BackgroundColorSpan(backgroundColor);
-            spannable.setSpan(backgroundColorSpan, 0, spannable.length(),
+            final int spanLength = Math.min(coloredTextLength, spannable.length());
+            spannable.setSpan(backgroundColorSpan, 0, spanLength,
                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
             composingTextToBeSet = spannable;
         }
@@ -2283,7 +2274,8 @@
     }
 
     /**
-     * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo} is called.
+     * Must be called from {@link InputMethodService#onUpdateCursorAnchorInfo(CursorAnchorInfo)} is
+     * called.
      * @param info The wrapper object with which we can access cursor/anchor info.
      */
     public void onUpdateCursorAnchorInfo(final CursorAnchorInfoCompatWrapper info) {
@@ -2307,12 +2299,12 @@
     }
 
     /**
-     * Returns whether the commit indicator should be shown or not.
-     * @param suggestedWords the suggested word that is being displayed.
+     * Returns whether the add to dictionary indicator should be shown or not.
+     * @param lastComposedWord the last composed word information.
      * @param settingsValues the current settings value.
      * @return {@code true} if the commit indicator should be shown.
      */
-    private boolean shouldShowCommitIndicator(final SuggestedWords suggestedWords,
+    private boolean shouldShowAddToDictionaryForTypedWord(final LastComposedWord lastComposedWord,
             final SettingsValues settingsValues) {
         if (!mConnection.isCursorAnchorInfoMonitorEnabled()) {
             // We cannot help in this case because we are heavily relying on this new API.
@@ -2321,24 +2313,16 @@
         if (!settingsValues.mShouldShowUiToAcceptTypedWord) {
             return false;
         }
-        final SuggestedWordInfo typedWordInfo = suggestedWords.getTypedWordInfoOrNull();
-        if (typedWordInfo == null) {
+        if (TextUtils.isEmpty(lastComposedWord.mTypedWord)) {
             return false;
         }
-        if (suggestedWords.mInputStyle != SuggestedWords.INPUT_STYLE_TYPING){
+        if (TextUtils.equals(lastComposedWord.mTypedWord, lastComposedWord.mCommittedWord)) {
             return false;
         }
-        if (settingsValues.mShowCommitIndicatorOnlyForAutoCorrection
-                && !suggestedWords.mWillAutoCorrect) {
+        if (!mDictionaryFacilitator.isUserDictionaryEnabled()) {
             return false;
         }
-        // TODO: Calling shouldShowAddToDictionaryHint(typedWordInfo) multiple times should be fine
-        // in terms of performance, but we can do better. One idea is to make SuggestedWords include
-        // a boolean that tells whether the word is a dictionary word or not.
-        if (settingsValues.mShowCommitIndicatorOnlyForOutOfVocabulary
-                && !shouldShowAddToDictionaryHint(typedWordInfo)) {
-            return false;
-        }
-        return true;
+        return !mDictionaryFacilitator.isValidWord(lastComposedWord.mTypedWord,
+                true /* ignoreCase */);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
new file mode 100644
index 0000000..06ab1e2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/settings/AccountsSettingsFragment.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.inputmethod.latin.settings;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.text.TextUtils;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.define.ProductionFlags;
+import com.android.inputmethod.latin.utils.LoginAccountUtils;
+
+import javax.annotation.Nullable;
+
+/**
+ * "Accounts & Privacy" settings sub screen.
+ *
+ * This settings sub screen handles the following preferences:
+ * <li> Account selection/management for IME
+ * <li> TODO: Sync preferences
+ * <li> TODO: Privacy preferences
+ */
+public final class AccountsSettingsFragment extends SubScreenFragment {
+    static final String PREF_ACCCOUNT_SWITCHER = "account_switcher";
+
+    private final DialogInterface.OnClickListener mAccountSelectedListener =
+            new AccountSelectedListener();
+    private final DialogInterface.OnClickListener mAccountSignedOutListener =
+            new AccountSignedOutListener();
+
+    @Override
+    public void onCreate(final Bundle icicle) {
+        super.onCreate(icicle);
+        addPreferencesFromResource(R.xml.prefs_screen_accounts);
+
+        final Resources res = getResources();
+        final Context context = getActivity();
+
+        // When we are called from the Settings application but we are not already running, some
+        // singleton and utility classes may not have been initialized.  We have to call
+        // initialization method of these classes here. See {@link LatinIME#onCreate()}.
+        SubtypeSwitcher.init(context);
+
+        if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) {
+            final Preference enableMetricsLogging =
+                    findPreference(Settings.PREF_ENABLE_METRICS_LOGGING);
+            if (enableMetricsLogging != null) {
+                final String enableMetricsLoggingTitle = res.getString(
+                        R.string.enable_metrics_logging, getApplicationName());
+                enableMetricsLogging.setTitle(enableMetricsLoggingTitle);
+            }
+        } else {
+            removePreference(Settings.PREF_ENABLE_METRICS_LOGGING);
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        refreshAccountSelection();
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
+        // TODO: Look at the preference that changed before refreshing the view.
+        refreshAccountSelection();
+    }
+
+    private void refreshAccountSelection() {
+        final String currentAccount = getCurrentlySelectedAccount();
+        final Preference accountSwitcher = findPreference(PREF_ACCCOUNT_SWITCHER);
+        if (currentAccount == null) {
+            // No account is currently selected.
+            accountSwitcher.setSummary(getString(R.string.no_accounts_selected));
+        } else {
+            // Set the currently selected account.
+            accountSwitcher.setSummary(getString(R.string.account_selected, currentAccount));
+        }
+        final Context context = getActivity();
+        final String[] accountsForLogin = LoginAccountUtils.getAccountsForLogin(context);
+        accountSwitcher.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                if (accountsForLogin.length == 0) {
+                    // TODO: Handle account addition.
+                    Toast.makeText(getActivity(),
+                            getString(R.string.account_select_cancel), Toast.LENGTH_SHORT).show();
+                } else {
+                    createAccountPicker(accountsForLogin, currentAccount).show();
+                }
+                return true;
+            }
+        });
+
+        // TODO: Depending on the account selection, enable/disable preferences that
+        // depend on an account.
+    }
+
+    @Nullable
+    private String getCurrentlySelectedAccount() {
+        return getSharedPreferences().getString(Settings.PREF_ACCOUNT_NAME, null);
+    }
+
+    /**
+     * Creates an account picker dialog showing the given accounts in a list and selecting
+     * the selected account by default.
+     * The list of accounts must not be null/empty.
+     *
+     * Package-private for testing.
+     */
+    AlertDialog createAccountPicker(final String[] accounts,
+            final String selectedAccount) {
+        if (accounts == null || accounts.length == 0) {
+            throw new IllegalArgumentException("List of accounts must not be empty");
+        }
+
+        // See if the currently selected account is in the list.
+        // If it is, the entry is selected, and a sign-out button is provided.
+        // If it isn't, select the 0th account by default which will get picked up
+        // if the user presses OK.
+        int index = 0;
+        boolean isSignedIn = false;
+        for (int i = 0;  i < accounts.length; i++) {
+            if (TextUtils.equals(accounts[i], selectedAccount)) {
+                index = i;
+                isSignedIn = true;
+                break;
+            }
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setTitle(R.string.account_select_title)
+                .setSingleChoiceItems(accounts, index, null)
+                .setPositiveButton(R.string.account_select_ok, mAccountSelectedListener)
+                .setNegativeButton(R.string.account_select_cancel, null);
+        if (isSignedIn) {
+            builder.setNeutralButton(R.string.account_select_sign_out, mAccountSignedOutListener);
+        }
+        return builder.create();
+    }
+
+    /**
+     * Listener for an account being selected from the picker.
+     * Persists the account to shared preferences.
+     */
+    class AccountSelectedListener implements DialogInterface.OnClickListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            final ListView lv = ((AlertDialog)dialog).getListView();
+            final Object selectedItem = lv.getItemAtPosition(lv.getCheckedItemPosition());
+            getSharedPreferences()
+                    .edit()
+                    .putString(Settings.PREF_ACCOUNT_NAME, (String) selectedItem)
+                    .apply();
+        }
+    }
+
+    /**
+     * Listener for sign-out being initiated from from the picker.
+     * Removed the account from shared preferences.
+     */
+    class AccountSignedOutListener implements DialogInterface.OnClickListener {
+        @Override
+        public void onClick(DialogInterface dialog, int which) {
+            getSharedPreferences()
+                    .edit()
+                    .remove(Settings.PREF_ACCOUNT_NAME)
+                    .apply();
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
index 00f2c73..a6cb55d 100644
--- a/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdvancedSettingsFragment.java
@@ -93,14 +93,16 @@
             removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON);
         }
 
+        // If metrics logging isn't supported, or account sign in is enabled
+        // don't show the logging preference.
+        // TODO: Eventually when we enable account sign in by default,
+        // we'll remove logging preference from here.
         if (ProductionFlags.IS_METRICS_LOGGING_SUPPORTED) {
             final Preference enableMetricsLogging =
                     findPreference(Settings.PREF_ENABLE_METRICS_LOGGING);
             if (enableMetricsLogging != null) {
-                final int applicationLabelRes = context.getApplicationInfo().labelRes;
-                final String applicationName = res.getString(applicationLabelRes);
                 final String enableMetricsLoggingTitle = res.getString(
-                        R.string.enable_metrics_logging, applicationName);
+                        R.string.enable_metrics_logging, getApplicationName());
                 enableMetricsLogging.setTitle(enableMetricsLoggingTitle);
             }
         } else {
diff --git a/java/src/com/android/inputmethod/latin/settings/Settings.java b/java/src/com/android/inputmethod/latin/settings/Settings.java
index 529f8a0..a171fc3 100644
--- a/java/src/com/android/inputmethod/latin/settings/Settings.java
+++ b/java/src/com/android/inputmethod/latin/settings/Settings.java
@@ -43,6 +43,7 @@
     private static final String TAG = Settings.class.getSimpleName();
     // Settings screens
     public static final String SCREEN_PREFERENCES = "screen_preferences";
+    public static final String SCREEN_ACCOUNTS = "screen_accounts";
     public static final String SCREEN_APPEARANCE = "screen_appearance";
     public static final String SCREEN_THEME = "screen_theme";
     public static final String SCREEN_MULTILINGUAL = "screen_multilingual";
@@ -104,6 +105,7 @@
     public static final String PREF_KEY_IS_INTERNAL = "pref_key_is_internal";
 
     public static final String PREF_ENABLE_METRICS_LOGGING = "pref_enable_metrics_logging";
+    public static final String PREF_ACCOUNT_NAME = "pref_account_name";
 
     // This preference key is deprecated. Use {@link #PREF_SHOW_LANGUAGE_SWITCH_KEY} instead.
     // This is being used only for the backward compatibility.
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
index 4fc1738..8c48017 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsFragment.java
@@ -25,6 +25,7 @@
 import android.view.MenuItem;
 
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.define.ProductionFlags;
 import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.FeedbackUtils;
 import com.android.inputmethodcommon.InputMethodSettingsFragment;
@@ -51,6 +52,10 @@
             final Preference multilingualOptions = findPreference(Settings.SCREEN_MULTILINGUAL);
             preferenceScreen.removePreference(multilingualOptions);
         }
+        if (!ProductionFlags.ENABLE_ACCOUNT_SIGN_IN) {
+            final Preference accountsPreference = findPreference(Settings.SCREEN_ACCOUNTS);
+            preferenceScreen.removePreference(accountsPreference);
+        }
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index 5761022..3339ab5 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -102,10 +102,7 @@
             new int[AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE];
 
     // TextDecorator
-    public final int mTextHighlightColorForCommitIndicator;
     public final int mTextHighlightColorForAddToDictionaryIndicator;
-    public final boolean mShowCommitIndicatorOnlyForAutoCorrection;
-    public final boolean mShowCommitIndicatorOnlyForOutOfVocabulary;
 
     // Debug settings
     public final boolean mIsInternal;
@@ -183,12 +180,6 @@
         mSuggestionsEnabledPerUserSettings = readSuggestionsEnabled(prefs);
         AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(
                 prefs, mAdditionalFeaturesSettingValues);
-        mShowCommitIndicatorOnlyForAutoCorrection = res.getBoolean(
-                R.bool.text_decorator_only_for_auto_correction);
-        mShowCommitIndicatorOnlyForOutOfVocabulary = res.getBoolean(
-                R.bool.text_decorator_only_for_out_of_vocabulary);
-        mTextHighlightColorForCommitIndicator = res.getColor(
-                R.color.text_decorator_commit_indicator_text_highlight_color);
         mTextHighlightColorForAddToDictionaryIndicator = res.getColor(
                 R.color.text_decorator_add_to_dictionary_indicator_text_highlight_color);
         mIsInternal = Settings.isInternal(prefs);
@@ -443,12 +434,6 @@
         sb.append("" + (null == awu ? "null" : awu.toString()));
         sb.append("\n   mAdditionalFeaturesSettingValues = ");
         sb.append("" + Arrays.toString(mAdditionalFeaturesSettingValues));
-        sb.append("\n   mShowCommitIndicatorOnlyForAutoCorrection = ");
-        sb.append("" + mShowCommitIndicatorOnlyForAutoCorrection);
-        sb.append("\n   mShowCommitIndicatorOnlyForOutOfVocabulary = ");
-        sb.append("" + mShowCommitIndicatorOnlyForOutOfVocabulary);
-        sb.append("\n   mTextHighlightColorForCommitIndicator = ");
-        sb.append("" + mTextHighlightColorForCommitIndicator);
         sb.append("\n   mTextHighlightColorForAddToDictionaryIndicator = ");
         sb.append("" + mTextHighlightColorForAddToDictionaryIndicator);
         sb.append("\n   mIsInternal = ");
diff --git a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
index ca5b395..240f8f8 100644
--- a/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/SubScreenFragment.java
@@ -20,6 +20,7 @@
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.Resources;
 import android.os.Bundle;
 import android.preference.ListPreference;
 import android.preference.Preference;
@@ -79,6 +80,16 @@
         return getPreferenceManager().getSharedPreferences();
     }
 
+    /**
+     * Gets the application name to display on the UI.
+     */
+    final String getApplicationName() {
+        final Context context = getActivity();
+        final Resources res = getResources();
+        final int applicationLabelRes = context.getApplicationInfo().labelRes;
+        return res.getString(applicationLabelRes);
+    }
+
     @Override
     public void addPreferencesFromResource(final int preferencesResId) {
         super.addPreferencesFromResource(preferencesResId);
diff --git a/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java
new file mode 100644
index 0000000..254bc65
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/settings/TestFragmentActivity.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.inputmethod.latin.settings;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Test activity to use when testing preference fragments. <br/>
+ * Usage: <br/>
+ * Create an ActivityInstrumentationTestCase2 for this activity
+ * and call setIntent() with an intent that specifies the fragment to load in the activity.
+ * The fragment can then be obtained from this activity and used for testing/verification.
+ */
+public final class TestFragmentActivity extends Activity {
+    /**
+     * The fragment name that should be loaded when starting this activity.
+     * This must be specified when starting this activity, as this activity is only
+     * meant to test fragments from instrumentation tests.
+     */
+    public static final String EXTRA_SHOW_FRAGMENT = "show_fragment";
+
+    public Fragment mFragment;
+
+    @Override
+    protected void onCreate(final Bundle savedState) {
+        super.onCreate(savedState);
+        final Intent intent = getIntent();
+        final String fragmentName = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
+        if (fragmentName == null) {
+            throw new IllegalArgumentException("No fragment name specified for testing");
+        }
+
+        mFragment = Fragment.instantiate(this, fragmentName);
+        FragmentManager fragmentManager = getFragmentManager();
+        fragmentManager.beginTransaction().add(mFragment, fragmentName).commit();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java
index c2167a7..ae2de44 100644
--- a/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/FragmentUtils.java
@@ -18,6 +18,7 @@
 
 import com.android.inputmethod.dictionarypack.DictionarySettingsFragment;
 import com.android.inputmethod.latin.about.AboutPreferences;
+import com.android.inputmethod.latin.settings.AccountsSettingsFragment;
 import com.android.inputmethod.latin.settings.AdvancedSettingsFragment;
 import com.android.inputmethod.latin.settings.AppearanceSettingsFragment;
 import com.android.inputmethod.latin.settings.CorrectionSettingsFragment;
@@ -42,6 +43,7 @@
         sLatinImeFragments.add(DictionarySettingsFragment.class.getName());
         sLatinImeFragments.add(AboutPreferences.class.getName());
         sLatinImeFragments.add(PreferencesSettingsFragment.class.getName());
+        sLatinImeFragments.add(AccountsSettingsFragment.class.getName());
         sLatinImeFragments.add(AppearanceSettingsFragment.class.getName());
         sLatinImeFragments.add(ThemeSettingsFragment.class.getName());
         sLatinImeFragments.add(MultiLingualSettingsFragment.class.getName());
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index 55557de..bbcef99 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -37,6 +37,14 @@
 
     private static final String EMPTY_STRING = "";
 
+    private static final char CHAR_LINE_FEED = 0X000A;
+    private static final char CHAR_VERTICAL_TAB = 0X000B;
+    private static final char CHAR_FORM_FEED = 0X000C;
+    private static final char CHAR_CARRIAGE_RETURN = 0X000D;
+    private static final char CHAR_NEXT_LINE = 0X0085;
+    private static final char CHAR_LINE_SEPARATOR = 0X2028;
+    private static final char CHAR_PARAGRAPH_SEPARATOR = 0X2029;
+
     private StringUtils() {
         // This utility class is not publicly instantiable.
     }
@@ -594,4 +602,30 @@
             return sb + "]";
         }
     }
+
+    /**
+     * Returns whether the last composed word contains line-breaking character (e.g. CR or LF).
+     * @param text the text to be examined.
+     * @return {@code true} if the last composed word contains line-breaking separator.
+     */
+    @UsedForTesting
+    public static boolean hasLineBreakCharacter(final String text) {
+        if (TextUtils.isEmpty(text)) {
+            return false;
+        }
+        for (int i = text.length() - 1; i >= 0; --i) {
+            final char c = text.charAt(i);
+            switch (c) {
+                case CHAR_LINE_FEED:
+                case CHAR_VERTICAL_TAB:
+                case CHAR_FORM_FEED:
+                case CHAR_CARRIAGE_RETURN:
+                case CHAR_NEXT_LINE:
+                case CHAR_LINE_SEPARATOR:
+                case CHAR_PARAGRAPH_SEPARATOR:
+                    return true;
+            }
+        }
+        return false;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java
index eaa5743..d6f6442 100644
--- a/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java
+++ b/java/src/com/android/inputmethod/latin/utils/SuggestionResults.java
@@ -22,7 +22,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
-import java.util.Locale;
 import java.util.TreeSet;
 
 /**
@@ -31,14 +30,17 @@
  */
 public final class SuggestionResults extends TreeSet<SuggestedWordInfo> {
     public final ArrayList<SuggestedWordInfo> mRawSuggestions;
+    // TODO: Instead of a boolean , we may want to include the context of this suggestion results,
+    // such as {@link PrevWordsInfo}.
+    public final boolean mIsBeginningOfSentence;
     private final int mCapacity;
 
-    public SuggestionResults(final int capacity) {
-        this(sSuggestedWordInfoComparator, capacity);
+    public SuggestionResults(final int capacity, final boolean isBeginningOfSentence) {
+        this(sSuggestedWordInfoComparator, capacity, isBeginningOfSentence);
     }
 
-    public SuggestionResults(final Comparator<SuggestedWordInfo> comparator,
-            final int capacity) {
+    private SuggestionResults(final Comparator<SuggestedWordInfo> comparator,
+            final int capacity, final boolean isBeginningOfSentence) {
         super(comparator);
         mCapacity = capacity;
         if (ProductionFlags.INCLUDE_RAW_SUGGESTIONS) {
@@ -46,6 +48,7 @@
         } else {
             mRawSuggestions = null;
         }
+        mIsBeginningOfSentence = isBeginningOfSentence;
     }
 
     @Override
diff --git a/tests/Android.mk b/tests/Android.mk
index 5baebbd..a084ad1 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -24,6 +24,8 @@
 # Do not compress test data file
 LOCAL_AAPT_FLAGS += -0 .txt
 
+LOCAL_STATIC_JAVA_LIBRARIES := mockito-target android-support-test
+
 # Include all test java files.
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 8a628cd..26195d3 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -26,7 +26,7 @@
         <!-- meta-data android:name="com.android.contacts.iconset" android:resource="@xml/iconset" /-->
     </application>
 
-    <instrumentation android:name="android.test.InstrumentationTestRunner"
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
         android:targetPackage="com.android.inputmethod.latin"
         android:label="LatinIME tests">
     </instrumentation>
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
index b64ab8c..5bca2dc 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
@@ -117,12 +117,12 @@
     protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
             final EditorInfo editorInfo) {
         return createKeyboardLayoutSet(subtype, editorInfo, false /* voiceInputKeyEnabled */,
-                false /* languageSwitchKeyEnabled */);
+                false /* languageSwitchKeyEnabled */, false /* splitLayoutEnabled */);
     }
 
     protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
             final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
-            final boolean languageSwitchKeyEnabled) {
+            final boolean languageSwitchKeyEnabled, final boolean splitLayoutEnabled) {
         final Context context = getContext();
         final Resources res = context.getResources();
         final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
@@ -131,7 +131,8 @@
         builder.setKeyboardGeometry(keyboardWidth, keyboardHeight)
                 .setSubtype(new RichInputMethodSubtype(subtype))
                 .setVoiceInputKeyEnabled(voiceInputKeyEnabled)
-                .setLanguageSwitchKeyEnabled(languageSwitchKeyEnabled);
+                .setLanguageSwitchKeyEnabled(languageSwitchKeyEnabled)
+                .setSplitLayoutEnabledByUser(splitLayoutEnabled);
         return builder.build();
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/EnglishSplitCustomizer.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/EnglishSplitCustomizer.java
new file mode 100644
index 0000000..b6d57d3
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/EnglishSplitCustomizer.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.inputmethod.keyboard.layout.tests;
+
+import com.android.inputmethod.keyboard.layout.LayoutBase;
+import com.android.inputmethod.keyboard.layout.expected.ExpectedKey;
+
+import java.util.Locale;
+
+public class EnglishSplitCustomizer extends EnglishCustomizer {
+
+    EnglishSplitCustomizer(Locale locale) {
+        super(locale);
+    }
+
+    @Override
+    public ExpectedKey[] getSpaceKeys(final boolean isPhone) {
+        return LayoutBase.joinKeys(
+                LayoutBase.LANGUAGE_SWITCH_KEY, LayoutBase.SPACE_KEY, LayoutBase.SPACE_KEY);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/LayoutTestsBase.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/LayoutTestsBase.java
index a22ed60..a8c4ac8 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/tests/LayoutTestsBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/LayoutTestsBase.java
@@ -54,7 +54,8 @@
                 + (isPhone() ? "phone" : "tablet");
         // TODO: Test with language switch key enabled and disabled.
         mKeyboardLayoutSet = createKeyboardLayoutSet(mSubtype, null /* editorInfo */,
-                true /* voiceInputKeyEnabled */, true /* languageSwitchKeyEnabled */);
+                true /* voiceInputKeyEnabled */, true /* languageSwitchKeyEnabled */,
+                false /* splitLayoutEnabled */);
     }
 
     @Override
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakEmail.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakEmail.java
index 37ca092..b25b846 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakEmail.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakEmail.java
@@ -44,12 +44,13 @@
     @Override
     protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
             final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
-            final boolean languageSwitchKeyEnabled) {
+            final boolean languageSwitchKeyEnabled, final boolean splitLayoutEnabled) {
         final EditorInfo emailField = new EditorInfo();
         emailField.inputType =
                 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
         return super.createKeyboardLayoutSet(
-                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled);
+                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled,
+                splitLayoutEnabled);
     }
 
     private static class DvorakEmailCustomizer extends EnglishDvorakCustomizer {
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakUrl.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakUrl.java
index 3bcae0c..ba22f37 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakUrl.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakUrl.java
@@ -44,12 +44,13 @@
     @Override
     protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
             final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
-            final boolean languageSwitchKeyEnabled) {
+            final boolean languageSwitchKeyEnabled, final boolean splitLayoutEnabled) {
         final EditorInfo emailField = new EditorInfo();
         emailField.inputType =
                 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI;
         return super.createKeyboardLayoutSet(
-                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled);
+                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled,
+                splitLayoutEnabled);
     }
 
     private static class DvorakUrlCustomizer extends EnglishDvorakCustomizer {
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyEmail.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyEmail.java
index 8563d69..f898632 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyEmail.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyEmail.java
@@ -42,12 +42,13 @@
     @Override
     protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
             final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
-            final boolean languageSwitchKeyEnabled) {
+            final boolean languageSwitchKeyEnabled, final boolean splitLayoutEnabled) {
         final EditorInfo emailField = new EditorInfo();
         emailField.inputType =
                 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
         return super.createKeyboardLayoutSet(
-                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled);
+                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled,
+                splitLayoutEnabled);
     }
 
     private static class EnglishEmailCustomizer extends EnglishCustomizer {
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyUrl.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyUrl.java
index 1c1a2bb..0b69c7b 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyUrl.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyUrl.java
@@ -42,12 +42,13 @@
     @Override
     protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
             final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
-            final boolean languageSwitchKeyEnabled) {
+            final boolean languageSwitchKeyEnabled, final boolean splitLayoutEnabled) {
         final EditorInfo emailField = new EditorInfo();
         emailField.inputType =
                 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI;
         return super.createKeyboardLayoutSet(
-                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled);
+                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled,
+                splitLayoutEnabled);
     }
 
     private static class EnglishUrlCustomizer extends EnglishCustomizer {
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsSplitLayoutQwertyEnglishUS.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsSplitLayoutQwertyEnglishUS.java
new file mode 100644
index 0000000..b9e40e0
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsSplitLayoutQwertyEnglishUS.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.inputmethod.keyboard.layout.tests;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.keyboard.layout.LayoutBase;
+import com.android.inputmethod.keyboard.layout.Qwerty;
+
+import java.util.Locale;
+
+/**
+ * en_US: English (United States)/qwerty - split layout
+ */
+@SmallTest
+public class TestsSplitLayoutQwertyEnglishUS extends LayoutTestsBase {
+    private static final Locale LOCALE = new Locale("en", "US");
+    private static final LayoutBase LAYOUT = new Qwerty(new EnglishSplitCustomizer(LOCALE));
+
+    @Override
+    protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
+            final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
+            final boolean languageSwitchKeyEnabled, final boolean splitLayoutEnabled) {
+        return super.createKeyboardLayoutSet(subtype, editorInfo, voiceInputKeyEnabled,
+            languageSwitchKeyEnabled, true /* splitLayoutEnabled */);
+    }
+
+    @Override
+    LayoutBase getLayout() { return LAYOUT; }
+}
diff --git a/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java b/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java
new file mode 100644
index 0000000..2ef8b54
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/settings/AccountsSettingsFragmentTests.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.inputmethod.latin.settings;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Intent;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.View;
+import android.widget.ListView;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@MediumTest
+public class AccountsSettingsFragmentTests
+        extends ActivityInstrumentationTestCase2<TestFragmentActivity> {
+    private static final String FRAG_NAME = AccountsSettingsFragment.class.getName();
+    private static final long TEST_TIMEOUT_MILLIS = 5000;
+
+    private AlertDialog mDialog;
+
+    public AccountsSettingsFragmentTests() {
+        super(TestFragmentActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        Intent intent = new Intent();
+        intent.putExtra(TestFragmentActivity.EXTRA_SHOW_FRAGMENT, FRAG_NAME);
+        setActivityIntent(intent);
+    }
+
+    public void testEmptyAccounts() {
+        final AccountsSettingsFragment fragment =
+                (AccountsSettingsFragment) getActivity().mFragment;
+        try {
+            fragment.createAccountPicker(new String[0], null);
+            fail("Expected IllegalArgumentException, never thrown");
+        } catch (IllegalArgumentException expected) {
+            // Expected.
+        }
+    }
+
+    public void testMultipleAccounts_noCurrentAccount() {
+        final AccountsSettingsFragment fragment =
+                (AccountsSettingsFragment) getActivity().mFragment;
+        final CountDownLatch latch = new CountDownLatch(1);
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mDialog = fragment.createAccountPicker(
+                        new String[] {
+                                "1@example.com",
+                                "2@example.com",
+                                "3@example.com",
+                                "4@example.com"},
+                        null);
+                mDialog.show();
+                latch.countDown();
+            }
+        });
+
+        try {
+            latch.await(TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ex) {
+            fail();
+        }
+        getInstrumentation().waitForIdleSync();
+        final ListView lv = mDialog.getListView();
+        // The 1st account should be checked by default.
+        assertEquals("checked-item", 0, lv.getCheckedItemPosition());
+        // There should be 4 accounts in the list.
+        assertEquals("count", 4, lv.getCount());
+        // The sign-out button shouldn't exist
+        assertEquals(View.GONE, mDialog.getButton(Dialog.BUTTON_NEUTRAL).getVisibility());
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_NEGATIVE).getVisibility());
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_POSITIVE).getVisibility());
+    }
+
+    public void testMultipleAccounts_currentAccount() {
+        final AccountsSettingsFragment fragment =
+                (AccountsSettingsFragment) getActivity().mFragment;
+        final CountDownLatch latch = new CountDownLatch(1);
+        getActivity().runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mDialog = fragment.createAccountPicker(
+                        new String[] {
+                                "1@example.com",
+                                "2@example.com",
+                                "3@example.com",
+                                "4@example.com"},
+                        "3@example.com");
+                mDialog.show();
+                latch.countDown();
+            }
+        });
+
+        try {
+            latch.await(TEST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException ex) {
+            fail();
+        }
+        getInstrumentation().waitForIdleSync();
+        final ListView lv = mDialog.getListView();
+        // The 3rd account should be checked by default.
+        assertEquals("checked-item", 2, lv.getCheckedItemPosition());
+        // There should be 4 accounts in the list.
+        assertEquals("count", 4, lv.getCount());
+        // The sign-out button should be shown
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_NEUTRAL).getVisibility());
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_NEGATIVE).getVisibility());
+        assertEquals(View.VISIBLE, mDialog.getButton(Dialog.BUTTON_POSITIVE).getVisibility());
+    }
+}