Merge "Native side reads character table"
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/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/dictionarypack/DownloadManagerWrapper.java b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java
index 75cc7d4..3dbbc9b 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DownloadManagerWrapper.java
@@ -54,15 +54,13 @@
             if (null != mDownloadManager) {
                 mDownloadManager.remove(ids);
             }
+        } catch (IllegalArgumentException e) {
+            // This is expected to happen on boot when the device is encrypted.
         } catch (SQLiteException e) {
             // We couldn't remove the file from DownloadManager. Apparently, the database can't
             // be opened. It may be a problem with file system corruption. In any case, there is
             // not much we can do apart from avoiding crashing.
             Log.e(TAG, "Can't remove files with ID " + ids + " from download manager", e);
-        } catch (IllegalArgumentException e) {
-            // Not sure how this can happen, but it could be another case where the provider
-            // is disabled. Or it could be a bug in older versions of the framework.
-            Log.e(TAG, "Can't find the content URL for DownloadManager?", e);
         }
     }
 
@@ -71,10 +69,10 @@
             if (null != mDownloadManager) {
                 return mDownloadManager.openDownloadedFile(fileId);
             }
+        } catch (IllegalArgumentException e) {
+            // This is expected to happen on boot when the device is encrypted.
         } catch (SQLiteException e) {
             Log.e(TAG, "Can't open downloaded file with ID " + fileId, e);
-        } catch (IllegalArgumentException e) {
-            Log.e(TAG, "Can't find the content URL for DownloadManager?", e);
         }
         // We come here if mDownloadManager is null or if an exception was thrown.
         throw new FileNotFoundException();
@@ -85,10 +83,10 @@
             if (null != mDownloadManager) {
                 return mDownloadManager.query(query);
             }
+        } catch (IllegalArgumentException e) {
+            // This is expected to happen on boot when the device is encrypted.
         } catch (SQLiteException e) {
             Log.e(TAG, "Can't query the download manager", e);
-        } catch (IllegalArgumentException e) {
-            Log.e(TAG, "Can't find the content URL for DownloadManager?", e);
         }
         // We come here if mDownloadManager is null or if an exception was thrown.
         return null;
@@ -99,10 +97,10 @@
             if (null != mDownloadManager) {
                 return mDownloadManager.enqueue(request);
             }
+        } catch (IllegalArgumentException e) {
+            // This is expected to happen on boot when the device is encrypted.
         } catch (SQLiteException e) {
             Log.e(TAG, "Can't enqueue a request with the download manager", e);
-        } catch (IllegalArgumentException e) {
-            Log.e(TAG, "Can't find the content URL for DownloadManager?", e);
         }
         return 0;
     }
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/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/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 8b13bdb..9bca0bf 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -186,9 +186,10 @@
             long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
             int[] pointerIds, int[] inputCodePoints, int inputSize, int[] suggestOptions,
             int[][] prevWordCodePointArrays, boolean[] isBeginningOfSentenceArray,
-            int[] outputSuggestionCount, int[] outputCodePoints, int[] outputScores,
-            int[] outputIndices, int[] outputTypes, int[] outputAutoCommitFirstWordConfidence,
-            float[] inOutLanguageWeight);
+            int prevWordCount, int[] outputSuggestionCount, int[] outputCodePoints,
+            int[] outputScores, int[] outputIndices, int[] outputTypes,
+            int[] outputAutoCommitFirstWordConfidence,
+            float[] inOutWeightOfLangModelVsSpatialModel);
     private static native boolean addUnigramEntryNative(long dict, int[] word, int probability,
             int[] shortcutTarget, int shortcutProbability, boolean isBeginningOfSentence,
             boolean isNotAWord, boolean isBlacklisted, int timestamp);
@@ -256,7 +257,8 @@
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
             final SettingsValuesForSuggestion settingsValuesForSuggestion,
-            final int sessionId, final float[] inOutLanguageWeight) {
+            final int sessionId, final float weightForLocale,
+            final float[] inOutWeightOfLangModelVsSpatialModel) {
         if (!isValidDictionary()) {
             return null;
         }
@@ -284,10 +286,12 @@
                 settingsValuesForSuggestion.mSpaceAwareGestureEnabled);
         session.mNativeSuggestOptions.setAdditionalFeaturesOptions(
                 settingsValuesForSuggestion.mAdditionalFeaturesSettingValues);
-        if (inOutLanguageWeight != null) {
-            session.mInputOutputLanguageWeight[0] = inOutLanguageWeight[0];
+        if (inOutWeightOfLangModelVsSpatialModel != null) {
+            session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
+                    inOutWeightOfLangModelVsSpatialModel[0];
         } else {
-            session.mInputOutputLanguageWeight[0] = Dictionary.NOT_A_LANGUAGE_WEIGHT;
+            session.mInputOutputWeightOfLangModelVsSpatialModel[0] =
+                    Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL;
         }
         // TOOD: Pass multiple previous words information for n-gram.
         getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
@@ -295,12 +299,14 @@
                 inputPointers.getYCoordinates(), inputPointers.getTimes(),
                 inputPointers.getPointerIds(), session.mInputCodePoints, inputSize,
                 session.mNativeSuggestOptions.getOptions(), session.mPrevWordCodePointArrays,
-                session.mIsBeginningOfSentenceArray, session.mOutputSuggestionCount,
-                session.mOutputCodePoints, session.mOutputScores, session.mSpaceIndices,
-                session.mOutputTypes, session.mOutputAutoCommitFirstWordConfidence,
-                session.mInputOutputLanguageWeight);
-        if (inOutLanguageWeight != null) {
-            inOutLanguageWeight[0] = session.mInputOutputLanguageWeight[0];
+                session.mIsBeginningOfSentenceArray, prevWordsInfo.getPrevWordCount(),
+                session.mOutputSuggestionCount, session.mOutputCodePoints, session.mOutputScores,
+                session.mSpaceIndices, session.mOutputTypes,
+                session.mOutputAutoCommitFirstWordConfidence,
+                session.mInputOutputWeightOfLangModelVsSpatialModel);
+        if (inOutWeightOfLangModelVsSpatialModel != null) {
+            inOutWeightOfLangModelVsSpatialModel[0] =
+                    session.mInputOutputWeightOfLangModelVsSpatialModel[0];
         }
         final int count = session.mOutputSuggestionCount[0];
         final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>();
@@ -314,7 +320,8 @@
             if (len > 0) {
                 suggestions.add(new SuggestedWordInfo(
                         new String(session.mOutputCodePoints, start, len),
-                        session.mOutputScores[j], session.mOutputTypes[j], this /* sourceDict */,
+                        (int)(session.mOutputScores[j] * weightForLocale), session.mOutputTypes[j],
+                        this /* sourceDict */,
                         session.mSpaceIndices[j] /* indexOfTouchPointOfSecondWord */,
                         session.mOutputAutoCommitFirstWordConfidence[0]));
             }
@@ -483,6 +490,7 @@
         return true;
     }
 
+    @UsedForTesting
     public void addMultipleDictionaryEntries(final LanguageModelParam[] languageModelParams) {
         if (!isValidDictionary()) return;
         int processedParamCount = 0;
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
index b341f62..2751c12 100644
--- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -40,7 +40,7 @@
     public final int[] mOutputTypes = new int[MAX_RESULTS];
     // Only one result is ever used
     public final int[] mOutputAutoCommitFirstWordConfidence = new int[1];
-    public final float[] mInputOutputLanguageWeight = new float[1];
+    public final float[] mInputOutputWeightOfLangModelVsSpatialModel = new float[1];
 
     public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
 
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index cad9ee7..b58a52b 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -31,7 +31,7 @@
  */
 public abstract class Dictionary {
     public static final int NOT_A_PROBABILITY = -1;
-    public static final float NOT_A_LANGUAGE_WEIGHT = -1.0f;
+    public static final float NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL = -1.0f;
 
     // The following types do not actually come from real dictionary instances, so we create
     // corresponding instances.
@@ -88,15 +88,18 @@
      * @param proximityInfo the object for key proximity. May be ignored by some implementations.
      * @param settingsValuesForSuggestion the settings values used for the suggestion.
      * @param sessionId the session id.
-     * @param inOutLanguageWeight the language weight used for generating suggestions.
-     * inOutLanguageWeight is a float array that has only one element. This can be updated when the
-     * different language weight is used.
+     * @param weightForLocale the weight given to this locale, to multiply the output scores for
+     * multilingual input.
+     * @param inOutWeightOfLangModelVsSpatialModel the weight of the language model as a ratio of
+     * the spatial model, used for generating suggestions. inOutWeightOfLangModelVsSpatialModel is
+     * a float array that has only one element. This can be updated when a different value is used.
      * @return the list of suggestions (possibly null if none)
      */
     abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
             final SettingsValuesForSuggestion settingsValuesForSuggestion,
-            final int sessionId, final float[] inOutLanguageWeight);
+            final int sessionId, final float weightForLocale,
+            final float[] inOutWeightOfLangModelVsSpatialModel);
 
     /**
      * Checks if the given word has to be treated as a valid word. Please note that some
@@ -190,7 +193,8 @@
         public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
                 final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
                 final SettingsValuesForSuggestion settingsValuesForSuggestion,
-                final int sessionId, final float[] inOutLanguageWeight) {
+                final int sessionId, final float weightForLocale,
+                final float[] inOutWeightOfLangModelVsSpatialModel) {
             return null;
         }
 
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index ca5e937..b26b378 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -62,20 +62,21 @@
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
             final SettingsValuesForSuggestion settingsValuesForSuggestion,
-            final int sessionId, final float[] inOutLanguageWeight) {
+            final int sessionId, final float weightForLocale,
+            final float[] inOutWeightOfLangModelVsSpatialModel) {
         final CopyOnWriteArrayList<Dictionary> dictionaries = mDictionaries;
         if (dictionaries.isEmpty()) return null;
         // To avoid creating unnecessary objects, we get the list out of the first
         // dictionary and add the rest to it if not null, hence the get(0)
         ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer,
                 prevWordsInfo, proximityInfo, settingsValuesForSuggestion, sessionId,
-                inOutLanguageWeight);
+                weightForLocale, inOutWeightOfLangModelVsSpatialModel);
         if (null == suggestions) suggestions = new ArrayList<>();
         final int length = dictionaries.size();
         for (int i = 1; i < length; ++ i) {
             final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
                     prevWordsInfo, proximityInfo, settingsValuesForSuggestion, sessionId,
-                    inOutLanguageWeight);
+                    weightForLocale, inOutWeightOfLangModelVsSpatialModel);
             if (null != sugg) suggestions.addAll(sugg);
         }
         return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index eced45e..aa15bd6 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -104,6 +104,7 @@
     private static class DictionaryGroup {
         public final Locale mLocale;
         private Dictionary mMainDict;
+        public float mWeightForLocale = 1.0f;
         public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap =
                 new ConcurrentHashMap<>();
 
@@ -595,16 +596,19 @@
             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 float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT };
+        final SuggestionResults suggestionResults = new SuggestionResults(
+                SuggestedWords.MAX_SUGGESTIONS,
+                prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence);
+        final float[] weightOfLangModelVsSpatialModel =
+                new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL };
         for (final DictionaryGroup dictionaryGroup : dictionaryGroups) {
             for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
                 final Dictionary dictionary = dictionaryGroup.getDict(dictType);
                 if (null == dictionary) continue;
                 final ArrayList<SuggestedWordInfo> dictionarySuggestions =
                         dictionary.getSuggestions(composer, prevWordsInfo, proximityInfo,
-                                settingsValuesForSuggestion, sessionId, languageWeight);
+                                settingsValuesForSuggestion, sessionId,
+                                dictionaryGroup.mWeightForLocale, weightOfLangModelVsSpatialModel);
                 if (null == dictionarySuggestions) continue;
                 suggestionResults.addAll(dictionarySuggestions);
                 if (null != suggestionResults.mRawSuggestions) {
@@ -706,6 +710,7 @@
                 getLocale(), personalizationDataChunk, spacingAndPunctuations, callback);
     }
 
+    @UsedForTesting
     public void addPhraseToContextualDictionary(final String[] phrase, final int probability,
             final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) {
         // TODO: we're inserting the phrase into the dictionary for the active language. Rethink
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 671ba67..ad967c1 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -435,7 +435,7 @@
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
             final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId,
-            final float[] inOutLanguageWeight) {
+            final float weightForLocale, final float[] inOutWeightOfLangModelVsSpatialModel) {
         reloadDictionaryIfRequired();
         boolean lockAcquired = false;
         try {
@@ -447,7 +447,8 @@
                 }
                 final ArrayList<SuggestedWordInfo> suggestions =
                         mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo,
-                                settingsValuesForSuggestion, sessionId, inOutLanguageWeight);
+                                settingsValuesForSuggestion, sessionId, weightForLocale,
+                                inOutWeightOfLangModelVsSpatialModel);
                 if (mBinaryDictionary.isCorrupted()) {
                     Log.i(TAG, "Dictionary (" + mDictName +") is corrupted. "
                             + "Remove and regenerate it.");
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index e2abb84..69fe6de 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1491,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/ReadOnlyBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
index ecf25c2..827367b 100644
--- a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
@@ -53,11 +53,13 @@
     public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
             final SettingsValuesForSuggestion settingsValuesForSuggestion,
-            final int sessionId, final float[] inOutLanguageWeight) {
+            final int sessionId, final float weightForLocale,
+            final float[] inOutWeightOfLangModelVsSpatialModel) {
         if (mLock.readLock().tryLock()) {
             try {
                 return mBinaryDictionary.getSuggestions(composer, prevWordsInfo, proximityInfo,
-                        settingsValuesForSuggestion, sessionId, inOutLanguageWeight);
+                        settingsValuesForSuggestion, sessionId, weightForLocale,
+                        inOutWeightOfLangModelVsSpatialModel);
             } finally {
                 mLock.readLock().unlock();
             }
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 1f0339c..8eccd5c 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -205,6 +205,8 @@
     public void finishInput() {
         if (mWordComposer.isComposingWord()) {
             mConnection.finishComposingText();
+            StatsUtils.onWordCommitUserTyped(
+                    mWordComposer.getTypedWord(), mWordComposer.isBatchMode());
         }
         resetComposingState(true /* alsoResetLastComposedWord */);
         mInputLogicHandler.reset();
@@ -251,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;
@@ -350,6 +353,8 @@
         }
 
         StatsUtils.onPickSuggestionManually(mSuggestedWords, suggestionInfo);
+        StatsUtils.onWordCommitSuggestionPickedManually(
+                suggestionInfo.mWord, mWordComposer.isBatchMode());
         return inputTransaction;
     }
 
@@ -573,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());
@@ -1064,8 +1070,10 @@
             inputTransaction.setRequiresUpdateSuggestions();
         } else {
             if (mLastComposedWord.canRevertCommit()) {
+                final String lastComposedWord = mLastComposedWord.mTypedWord;
                 revertCommit(inputTransaction, inputTransaction.mSettingsValues);
                 StatsUtils.onRevertAutoCorrect();
+                StatsUtils.onWordCommitUserTyped(lastComposedWord, mWordComposer.isBatchMode());
                 return;
             }
             if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
@@ -2001,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);
@@ -2039,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);
         }
     }
 
@@ -2086,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)) {
@@ -2098,8 +2111,11 @@
                 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);
             }
         }
     }
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/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/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index 1979080..2494787 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -23,6 +23,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.AssetFileAddress;
 import com.android.inputmethod.latin.BinaryDictionaryGetter;
 import com.android.inputmethod.latin.Constants;
@@ -382,6 +383,7 @@
         return dictList;
     }
 
+    @UsedForTesting
     public static boolean looksValidForDictionaryInsertion(final CharSequence text,
             final SpacingAndPunctuations spacingAndPunctuations) {
         if (TextUtils.isEmpty(text)) return false;
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
index 94c6242..6fd241e 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
@@ -21,6 +21,7 @@
 
 import android.view.inputmethod.InputMethodSubtype;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.PrevWordsInfo;
 
 public interface DistracterFilter {
@@ -36,6 +37,7 @@
     public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo,
             final String testedWord, final Locale locale);
 
+    @UsedForTesting
     public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, final String testedWord,
             final Locale locale);
 
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/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
index 05d1247..7955541 100644
--- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
+++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
@@ -18,6 +18,7 @@
 
 import android.util.Log;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.DictionaryFacilitator;
 import com.android.inputmethod.latin.PrevWordsInfo;
@@ -58,12 +59,14 @@
     public final int mTimestamp;
 
     // Constructor for unigram. TODO: support shortcuts
+    @UsedForTesting
     public LanguageModelParam(final CharSequence word, final int unigramProbability,
             final int timestamp) {
         this(null /* word0 */, word, unigramProbability, Dictionary.NOT_A_PROBABILITY, timestamp);
     }
 
     // Constructor for unigram and bigram.
+    @UsedForTesting
     public LanguageModelParam(final CharSequence word0, final CharSequence word1,
             final int unigramProbability, final int bigramProbability,
             final int timestamp) {
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/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 440b963..688ce44 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -180,9 +180,10 @@
         jintArray yCoordinatesArray, jintArray timesArray, jintArray pointerIdsArray,
         jintArray inputCodePointsArray, jint inputSize, jintArray suggestOptions,
         jobjectArray prevWordCodePointArrays, jbooleanArray isBeginningOfSentenceArray,
-        jintArray outSuggestionCount, jintArray outCodePointsArray, jintArray outScoresArray,
-        jintArray outSpaceIndicesArray, jintArray outTypesArray,
-        jintArray outAutoCommitFirstWordConfidenceArray, jfloatArray inOutLanguageWeight) {
+        jint prevWordCount, jintArray outSuggestionCount, jintArray outCodePointsArray,
+        jintArray outScoresArray, jintArray outSpaceIndicesArray, jintArray outTypesArray,
+        jintArray outAutoCommitFirstWordConfidenceArray,
+        jfloatArray inOutWeightOfLangModelVsSpatialModel) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     // Assign 0 to outSuggestionCount here in case of returning earlier in this method.
     JniDataUtils::putIntToArray(env, outSuggestionCount, 0 /* index */, 0);
@@ -237,22 +238,23 @@
         ASSERT(false);
         return;
     }
-    float languageWeight;
-    env->GetFloatArrayRegion(inOutLanguageWeight, 0, 1 /* len */, &languageWeight);
+    float weightOfLangModelVsSpatialModel;
+    env->GetFloatArrayRegion(inOutWeightOfLangModelVsSpatialModel, 0, 1 /* len */,
+            &weightOfLangModelVsSpatialModel);
     SuggestionResults suggestionResults(MAX_RESULTS);
     const PrevWordsInfo prevWordsInfo = JniDataUtils::constructPrevWordsInfo(env,
-            prevWordCodePointArrays, isBeginningOfSentenceArray);
+            prevWordCodePointArrays, isBeginningOfSentenceArray, prevWordCount);
     if (givenSuggestOptions.isGesture() || inputSize > 0) {
         // TODO: Use SuggestionResults to return suggestions.
         dictionary->getSuggestions(pInfo, traverseSession, xCoordinates, yCoordinates,
                 times, pointerIds, inputCodePoints, inputSize, &prevWordsInfo,
-                &givenSuggestOptions, languageWeight, &suggestionResults);
+                &givenSuggestOptions, weightOfLangModelVsSpatialModel, &suggestionResults);
     } else {
         dictionary->getPredictions(&prevWordsInfo, &suggestionResults);
     }
     suggestionResults.outputSuggestions(env, outSuggestionCount, outCodePointsArray,
             outScoresArray, outSpaceIndicesArray, outTypesArray,
-            outAutoCommitFirstWordConfidenceArray, inOutLanguageWeight);
+            outAutoCommitFirstWordConfidenceArray, inOutWeightOfLangModelVsSpatialModel);
 }
 
 static jint latinime_BinaryDictionary_getProbability(JNIEnv *env, jclass clazz, jlong dict,
@@ -285,7 +287,8 @@
     int wordCodePoints[wordLength];
     env->GetIntArrayRegion(word, 0, wordLength, wordCodePoints);
     const PrevWordsInfo prevWordsInfo = JniDataUtils::constructPrevWordsInfo(env,
-            prevWordCodePointArrays, isBeginningOfSentenceArray);
+            prevWordCodePointArrays, isBeginningOfSentenceArray,
+            env->GetArrayLength(prevWordCodePointArrays));
     return dictionary->getNgramProbability(&prevWordsInfo,
             CodePointArrayView(wordCodePoints, wordLength));
 }
@@ -393,7 +396,8 @@
         return false;
     }
     const PrevWordsInfo prevWordsInfo = JniDataUtils::constructPrevWordsInfo(env,
-            prevWordCodePointArrays, isBeginningOfSentenceArray);
+            prevWordCodePointArrays, isBeginningOfSentenceArray,
+            env->GetArrayLength(prevWordCodePointArrays));
     jsize wordLength = env->GetArrayLength(word);
     int wordCodePoints[wordLength];
     env->GetIntArrayRegion(word, 0, wordLength, wordCodePoints);
@@ -413,7 +417,8 @@
         return false;
     }
     const PrevWordsInfo prevWordsInfo = JniDataUtils::constructPrevWordsInfo(env,
-            prevWordCodePointArrays, isBeginningOfSentenceArray);
+            prevWordCodePointArrays, isBeginningOfSentenceArray,
+            env->GetArrayLength(prevWordCodePointArrays));
     jsize codePointCount = env->GetArrayLength(word);
     int wordCodePoints[codePointCount];
     env->GetIntArrayRegion(word, 0, codePointCount, wordCodePoints);
@@ -667,7 +672,7 @@
     },
     {
         const_cast<char *>("getSuggestionsNative"),
-        const_cast<char *>("(JJJ[I[I[I[I[II[I[[I[Z[I[I[I[I[I[I[F)V"),
+        const_cast<char *>("(JJJ[I[I[I[I[II[I[[I[ZI[I[I[I[I[I[I[F)V"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_getSuggestions)
     },
     {
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index 57e1888..e55c9eb 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -301,7 +301,7 @@
 #define NOT_A_DICT_POS (S_INT_MIN)
 #define NOT_A_WORD_ID (S_INT_MIN)
 #define NOT_A_TIMESTAMP (-1)
-#define NOT_A_LANGUAGE_WEIGHT (-1.0f)
+#define NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL (-1.0f)
 
 // A special value to mean the first word confidence makes no sense in this case,
 // e.g. this is not a multi-word suggestion.
@@ -338,7 +338,7 @@
 #define MAX_POINTER_COUNT_G 2
 
 // (MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1)-gram is supported.
-#define MAX_PREV_WORD_COUNT_FOR_N_GRAM 1
+#define MAX_PREV_WORD_COUNT_FOR_N_GRAM 2
 
 #define DISALLOW_DEFAULT_CONSTRUCTOR(TypeName) \
   TypeName() = delete
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index ec61783..5214077 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -295,8 +295,9 @@
     }
 
     // Used to prune nodes
-    float getCompoundDistance(const float languageWeight) const {
-        return mDicNodeState.mDicNodeStateScoring.getCompoundDistance(languageWeight);
+    float getCompoundDistance(const float weightOfLangModelVsSpatialModel) const {
+        return mDicNodeState.mDicNodeStateScoring.getCompoundDistance(
+                weightOfLangModelVsSpatialModel);
     }
 
     AK_FORCE_INLINE const int *getOutputWordBuf() const {
diff --git a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_scoring.h b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_scoring.h
index c19d48e..3a54c25 100644
--- a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_scoring.h
+++ b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_scoring.h
@@ -103,8 +103,10 @@
         return getCompoundDistance(1.0f);
     }
 
-    float getCompoundDistance(const float languageWeight) const {
-        return mSpatialDistance + mLanguageDistance * languageWeight;
+    float getCompoundDistance(
+            const float weightOfLangModelVsSpatialModel) const {
+        return mSpatialDistance
+                + mLanguageDistance * weightOfLangModelVsSpatialModel;
     }
 
     float getNormalizedCompoundDistance() const {
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.cpp b/native/jni/src/suggest/core/dictionary/dictionary.cpp
index f9f36ce..e4084b0 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.cpp
+++ b/native/jni/src/suggest/core/dictionary/dictionary.cpp
@@ -47,14 +47,14 @@
 void Dictionary::getSuggestions(ProximityInfo *proximityInfo, DicTraverseSession *traverseSession,
         int *xcoordinates, int *ycoordinates, int *times, int *pointerIds, int *inputCodePoints,
         int inputSize, const PrevWordsInfo *const prevWordsInfo,
-        const SuggestOptions *const suggestOptions, const float languageWeight,
+        const SuggestOptions *const suggestOptions, const float weightOfLangModelVsSpatialModel,
         SuggestionResults *const outSuggestionResults) const {
     TimeKeeper::setCurrentTime();
     traverseSession->init(this, prevWordsInfo, suggestOptions);
     const auto &suggest = suggestOptions->isGesture() ? mGestureSuggest : mTypingSuggest;
     suggest->getSuggestions(proximityInfo, traverseSession, xcoordinates,
             ycoordinates, times, pointerIds, inputCodePoints, inputSize,
-            languageWeight, outSuggestionResults);
+            weightOfLangModelVsSpatialModel, outSuggestionResults);
     if (DEBUG_DICT) {
         outSuggestionResults->dumpSuggestions();
     }
diff --git a/native/jni/src/suggest/core/dictionary/dictionary.h b/native/jni/src/suggest/core/dictionary/dictionary.h
index f6482ab..324e350 100644
--- a/native/jni/src/suggest/core/dictionary/dictionary.h
+++ b/native/jni/src/suggest/core/dictionary/dictionary.h
@@ -66,7 +66,7 @@
     void getSuggestions(ProximityInfo *proximityInfo, DicTraverseSession *traverseSession,
             int *xcoordinates, int *ycoordinates, int *times, int *pointerIds, int *inputCodePoints,
             int inputSize, const PrevWordsInfo *const prevWordsInfo,
-            const SuggestOptions *const suggestOptions, const float languageWeight,
+            const SuggestOptions *const suggestOptions, const float weightOfLangModelVsSpatialModel,
             SuggestionResults *const outSuggestionResults) const;
 
     void getPredictions(const PrevWordsInfo *const prevWordsInfo,
diff --git a/native/jni/src/suggest/core/policy/scoring.h b/native/jni/src/suggest/core/policy/scoring.h
index 9e75cac..ce3684a 100644
--- a/native/jni/src/suggest/core/policy/scoring.h
+++ b/native/jni/src/suggest/core/policy/scoring.h
@@ -32,9 +32,11 @@
             const ErrorTypeUtils::ErrorType containedErrorTypes, const bool forceCommit,
             const bool boostExactMatches) const = 0;
     virtual void getMostProbableString(const DicTraverseSession *const traverseSession,
-            const float languageWeight, SuggestionResults *const outSuggestionResults) const = 0;
-    virtual float getAdjustedLanguageWeight(DicTraverseSession *const traverseSession,
-            DicNode *const terminals, const int size) const = 0;
+            const float weightOfLangModelVsSpatialModel,
+            SuggestionResults *const outSuggestionResults) const = 0;
+    virtual float getAdjustedWeightOfLangModelVsSpatialModel(
+            DicTraverseSession *const traverseSession, DicNode *const terminals,
+            const int size) const = 0;
     virtual float getDoubleLetterDemotionDistanceCost(
             const DicNode *const terminalDicNode) const = 0;
     virtual bool autoCorrectsToMultiWordSuggestionIfTop() const = 0;
diff --git a/native/jni/src/suggest/core/result/suggestion_results.cpp b/native/jni/src/suggest/core/result/suggestion_results.cpp
index 4c10bd0..3756d10 100644
--- a/native/jni/src/suggest/core/result/suggestion_results.cpp
+++ b/native/jni/src/suggest/core/result/suggestion_results.cpp
@@ -23,7 +23,7 @@
 void SuggestionResults::outputSuggestions(JNIEnv *env, jintArray outSuggestionCount,
         jintArray outputCodePointsArray, jintArray outScoresArray, jintArray outSpaceIndicesArray,
         jintArray outTypesArray, jintArray outAutoCommitFirstWordConfidenceArray,
-        jfloatArray outLanguageWeight) {
+        jfloatArray outWeightOfLangModelVsSpatialModel) {
     int outputIndex = 0;
     while (!mSuggestedWords.empty()) {
         const SuggestedWord &suggestedWord = mSuggestedWords.top();
@@ -44,7 +44,8 @@
         mSuggestedWords.pop();
     }
     JniDataUtils::putIntToArray(env, outSuggestionCount, 0 /* index */, outputIndex);
-    JniDataUtils::putFloatToArray(env, outLanguageWeight, 0 /* index */, mLanguageWeight);
+    JniDataUtils::putFloatToArray(env, outWeightOfLangModelVsSpatialModel, 0 /* index */,
+            mWeightOfLangModelVsSpatialModel);
 }
 
 void SuggestionResults::addPrediction(const int *const codePoints, const int codePointCount,
@@ -89,7 +90,7 @@
 }
 
 void SuggestionResults::dumpSuggestions() const {
-    AKLOGE("language weight: %f", mLanguageWeight);
+    AKLOGE("weight of language model vs spatial model: %f", mWeightOfLangModelVsSpatialModel);
     std::vector<SuggestedWord> suggestedWords;
     auto copyOfSuggestedWords = mSuggestedWords;
     while (!copyOfSuggestedWords.empty()) {
diff --git a/native/jni/src/suggest/core/result/suggestion_results.h b/native/jni/src/suggest/core/result/suggestion_results.h
index 8e845e2..738c78a 100644
--- a/native/jni/src/suggest/core/result/suggestion_results.h
+++ b/native/jni/src/suggest/core/result/suggestion_results.h
@@ -29,13 +29,15 @@
 class SuggestionResults {
  public:
     explicit SuggestionResults(const int maxSuggestionCount)
-            : mMaxSuggestionCount(maxSuggestionCount), mLanguageWeight(NOT_A_LANGUAGE_WEIGHT),
+            : mMaxSuggestionCount(maxSuggestionCount),
+              mWeightOfLangModelVsSpatialModel(NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL),
               mSuggestedWords() {}
 
     // Returns suggestion count.
     void outputSuggestions(JNIEnv *env, jintArray outSuggestionCount, jintArray outCodePointsArray,
             jintArray outScoresArray, jintArray outSpaceIndicesArray, jintArray outTypesArray,
-            jintArray outAutoCommitFirstWordConfidenceArray, jfloatArray outLanguageWeight);
+            jintArray outAutoCommitFirstWordConfidenceArray,
+            jfloatArray outWeightOfLangModelVsSpatialModel);
     void addPrediction(const int *const codePoints, const int codePointCount, const int score);
     void addSuggestion(const int *const codePoints, const int codePointCount,
             const int score, const int type, const int indexToPartialCommit,
@@ -43,8 +45,8 @@
     void getSortedScores(int *const outScores) const;
     void dumpSuggestions() const;
 
-    void setLanguageWeight(const float languageWeight) {
-        mLanguageWeight = languageWeight;
+    void setWeightOfLangModelVsSpatialModel(const float weightOfLangModelVsSpatialModel) {
+        mWeightOfLangModelVsSpatialModel = weightOfLangModelVsSpatialModel;
     }
 
     int getSuggestionCount() const {
@@ -55,7 +57,7 @@
     DISALLOW_IMPLICIT_CONSTRUCTORS(SuggestionResults);
 
     const int mMaxSuggestionCount;
-    float mLanguageWeight;
+    float mWeightOfLangModelVsSpatialModel;
     std::priority_queue<
             SuggestedWord, std::vector<SuggestedWord>, SuggestedWord::Comparator> mSuggestedWords;
 };
diff --git a/native/jni/src/suggest/core/result/suggestions_output_utils.cpp b/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
index 6e01937..3283f6d 100644
--- a/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
+++ b/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
@@ -34,7 +34,8 @@
 
 /* static */ void SuggestionsOutputUtils::outputSuggestions(
         const Scoring *const scoringPolicy, DicTraverseSession *traverseSession,
-        const float languageWeight, SuggestionResults *const outSuggestionResults) {
+        const float weightOfLangModelVsSpatialModel,
+        SuggestionResults *const outSuggestionResults) {
 #if DEBUG_EVALUATE_MOST_PROBABLE_STRING
     const int terminalSize = 0;
 #else
@@ -44,12 +45,15 @@
     for (int index = terminalSize - 1; index >= 0; --index) {
         traverseSession->getDicTraverseCache()->popTerminal(&terminals[index]);
     }
-    // Compute a language weight when an invalid language weight is passed.
-    // NOT_A_LANGUAGE_WEIGHT (-1) is assumed as an invalid language weight.
-    const float languageWeightToOutputSuggestions = (languageWeight < 0.0f) ?
-            scoringPolicy->getAdjustedLanguageWeight(
-                    traverseSession, terminals.data(), terminalSize) : languageWeight;
-    outSuggestionResults->setLanguageWeight(languageWeightToOutputSuggestions);
+    // Compute a weight of language model when an invalid weight is passed.
+    // NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL (-1) is taken as an invalid value.
+    const float weightOfLangModelVsSpatialModelToOutputSuggestions =
+            (weightOfLangModelVsSpatialModel < 0.0f)
+            ? scoringPolicy->getAdjustedWeightOfLangModelVsSpatialModel(traverseSession,
+                    terminals.data(), terminalSize)
+            : weightOfLangModelVsSpatialModel;
+    outSuggestionResults->setWeightOfLangModelVsSpatialModel(
+            weightOfLangModelVsSpatialModelToOutputSuggestions);
     // Force autocorrection for obvious long multi-word suggestions when the top suggestion is
     // a long multiple words suggestion.
     // TODO: Implement a smarter auto-commit method for handling multi-word suggestions.
@@ -65,16 +69,16 @@
     // Output suggestion results here
     for (auto &terminalDicNode : terminals) {
         outputSuggestionsOfDicNode(scoringPolicy, traverseSession, &terminalDicNode,
-                languageWeightToOutputSuggestions, boostExactMatches, forceCommitMultiWords,
-                outputSecondWordFirstLetterInputIndex, outSuggestionResults);
+                weightOfLangModelVsSpatialModelToOutputSuggestions, boostExactMatches,
+                forceCommitMultiWords, outputSecondWordFirstLetterInputIndex, outSuggestionResults);
     }
-    scoringPolicy->getMostProbableString(traverseSession, languageWeightToOutputSuggestions,
-            outSuggestionResults);
+    scoringPolicy->getMostProbableString(traverseSession,
+            weightOfLangModelVsSpatialModelToOutputSuggestions, outSuggestionResults);
 }
 
 /* static */ void SuggestionsOutputUtils::outputSuggestionsOfDicNode(
         const Scoring *const scoringPolicy, DicTraverseSession *traverseSession,
-        const DicNode *const terminalDicNode, const float languageWeight,
+        const DicNode *const terminalDicNode, const float weightOfLangModelVsSpatialModel,
         const bool boostExactMatches, const bool forceCommitMultiWords,
         const bool outputSecondWordFirstLetterInputIndex,
         SuggestionResults *const outSuggestionResults) {
@@ -83,8 +87,9 @@
     }
     const float doubleLetterCost =
             scoringPolicy->getDoubleLetterDemotionDistanceCost(terminalDicNode);
-    const float compoundDistance = terminalDicNode->getCompoundDistance(languageWeight)
-            + doubleLetterCost;
+    const float compoundDistance =
+            terminalDicNode->getCompoundDistance(weightOfLangModelVsSpatialModel)
+                    + doubleLetterCost;
     const WordAttributes wordAttributes = traverseSession->getDictionaryStructurePolicy()
             ->getWordAttributesInContext(terminalDicNode->getPrevWordIds(),
                     terminalDicNode->getWordId(), nullptr /* multiBigramMap */);
diff --git a/native/jni/src/suggest/core/result/suggestions_output_utils.h b/native/jni/src/suggest/core/result/suggestions_output_utils.h
index b099b47..bf84978 100644
--- a/native/jni/src/suggest/core/result/suggestions_output_utils.h
+++ b/native/jni/src/suggest/core/result/suggestions_output_utils.h
@@ -33,7 +33,7 @@
      * Outputs the final list of suggestions (i.e., terminal nodes).
      */
     static void outputSuggestions(const Scoring *const scoringPolicy,
-            DicTraverseSession *traverseSession, const float languageWeight,
+            DicTraverseSession *traverseSession, const float weightOfLangModelVsSpatialModel,
             SuggestionResults *const outSuggestionResults);
 
  private:
@@ -44,7 +44,7 @@
 
     static void outputSuggestionsOfDicNode(const Scoring *const scoringPolicy,
             DicTraverseSession *traverseSession, const DicNode *const terminalDicNode,
-            const float languageWeight, const bool boostExactMatches,
+            const float weightOfLangModelVsSpatialModel, const bool boostExactMatches,
             const bool forceCommitMultiWords, const bool outputSecondWordFirstLetterInputIndex,
             SuggestionResults *const outSuggestionResults);
     static void outputShortcuts(BinaryDictionaryShortcutIterator *const shortcutIt,
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index 947d41f..457414f 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -45,7 +45,7 @@
  */
 void Suggest::getSuggestions(ProximityInfo *pInfo, void *traverseSession,
         int *inputXs, int *inputYs, int *times, int *pointerIds, int *inputCodePoints,
-        int inputSize, const float languageWeight,
+        int inputSize, const float weightOfLangModelVsSpatialModel,
         SuggestionResults *const outSuggestionResults) const {
     PROF_OPEN;
     PROF_START(0);
@@ -68,7 +68,7 @@
     PROF_END(1);
     PROF_START(2);
     SuggestionsOutputUtils::outputSuggestions(
-            SCORING, tSession, languageWeight, outSuggestionResults);
+            SCORING, tSession, weightOfLangModelVsSpatialModel, outSuggestionResults);
     PROF_END(2);
     PROF_CLOSE;
 }
diff --git a/native/jni/src/suggest/core/suggest.h b/native/jni/src/suggest/core/suggest.h
index 788e031..65d5918 100644
--- a/native/jni/src/suggest/core/suggest.h
+++ b/native/jni/src/suggest/core/suggest.h
@@ -49,7 +49,8 @@
     AK_FORCE_INLINE virtual ~Suggest() {}
     void getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs, int *inputYs,
             int *times, int *pointerIds, int *inputCodePoints, int inputSize,
-            const float languageWeight, SuggestionResults *const outSuggestionResults) const;
+            const float weightOfLangModelVsSpatialModel,
+            SuggestionResults *const outSuggestionResults) const;
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Suggest);
diff --git a/native/jni/src/suggest/core/suggest_interface.h b/native/jni/src/suggest/core/suggest_interface.h
index a6e5aef..a05aa9c 100644
--- a/native/jni/src/suggest/core/suggest_interface.h
+++ b/native/jni/src/suggest/core/suggest_interface.h
@@ -28,7 +28,8 @@
  public:
     virtual void getSuggestions(ProximityInfo *pInfo, void *traverseSession, int *inputXs,
             int *inputYs, int *times, int *pointerIds, int *inputCodePoints, int inputSize,
-            const float languageWeight, SuggestionResults *const suggestionResults) const = 0;
+            const float weightOfLangModelVsSpatialModel,
+            SuggestionResults *const suggestionResults) const = 0;
     SuggestInterface() {}
     virtual ~SuggestInterface() {}
  private:
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_scoring.h b/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
index 52c4251..0240bcf 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
@@ -33,10 +33,12 @@
     static const TypingScoring *getInstance() { return &sInstance; }
 
     AK_FORCE_INLINE void getMostProbableString(const DicTraverseSession *const traverseSession,
-            const float languageWeight, SuggestionResults *const outSuggestionResults) const {}
+            const float weightOfLangModelVsSpatialModel,
+            SuggestionResults *const outSuggestionResults) const {}
 
-    AK_FORCE_INLINE float getAdjustedLanguageWeight(DicTraverseSession *const traverseSession,
-            DicNode *const terminals, const int size) const {
+    AK_FORCE_INLINE float getAdjustedWeightOfLangModelVsSpatialModel(
+            DicTraverseSession *const traverseSession, DicNode *const terminals,
+            const int size) const {
         return 1.0f;
     }
 
diff --git a/native/jni/src/utils/jni_data_utils.h b/native/jni/src/utils/jni_data_utils.h
index cb82d3c..235a03b 100644
--- a/native/jni/src/utils/jni_data_utils.h
+++ b/native/jni/src/utils/jni_data_utils.h
@@ -97,17 +97,13 @@
     }
 
     static PrevWordsInfo constructPrevWordsInfo(JNIEnv *env, jobjectArray prevWordCodePointArrays,
-            jbooleanArray isBeginningOfSentenceArray) {
+            jbooleanArray isBeginningOfSentenceArray, const size_t prevWordCount) {
         int prevWordCodePoints[MAX_PREV_WORD_COUNT_FOR_N_GRAM][MAX_WORD_LENGTH];
         int prevWordCodePointCount[MAX_PREV_WORD_COUNT_FOR_N_GRAM];
         bool isBeginningOfSentence[MAX_PREV_WORD_COUNT_FOR_N_GRAM];
-        jsize prevWordsCount = env->GetArrayLength(prevWordCodePointArrays);
-        for (size_t i = 0; i < NELEMS(prevWordCodePoints); ++i) {
+        for (size_t i = 0; i < prevWordCount; ++i) {
             prevWordCodePointCount[i] = 0;
             isBeginningOfSentence[i] = false;
-            if (prevWordsCount <= static_cast<int>(i)) {
-                continue;
-            }
             jintArray prevWord = (jintArray)env->GetObjectArrayElement(prevWordCodePointArrays, i);
             if (!prevWord) {
                 continue;
@@ -124,7 +120,7 @@
             isBeginningOfSentence[i] = isBeginningOfSentenceBoolean == JNI_TRUE;
         }
         return PrevWordsInfo(prevWordCodePoints, prevWordCodePointCount, isBeginningOfSentence,
-                MAX_PREV_WORD_COUNT_FOR_N_GRAM);
+                prevWordCount);
     }
 
     static void putBooleanToArray(JNIEnv *env, jbooleanArray array, const int index,
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());
+    }
+}