Merge "Remove unigram probability from dicNode."
diff --git a/java-overridable/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdater.java b/java-overridable/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdater.java
index c97a0d2..8b66cff 100644
--- a/java-overridable/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdater.java
+++ b/java-overridable/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryUpdater.java
@@ -16,8 +16,6 @@
 
 package com.android.inputmethod.latin.personalization;
 
-import java.util.Locale;
-
 import android.content.Context;
 
 import com.android.inputmethod.latin.DictionaryFacilitator;
@@ -33,12 +31,7 @@
         mDictionaryFacilitator = dictionaryFacilitator;
     }
 
-    public Locale getLocale() {
-        return null;
-    }
-
-    public void onLoadSettings(final boolean usePersonalizedDicts,
-            final boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes) {
+    public void onLoadSettings(final boolean usePersonalizedDicts) {
         if (!mDictCleared) {
             // Clear and never update the personalization dictionary.
             PersonalizationHelper.removeAllPersonalizationDictionaries(mContext);
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 c867fee..198afeb 100644
--- a/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtils.java
+++ b/java-overridable/src/com/android/inputmethod/latin/utils/StatsUtils.java
@@ -28,6 +28,26 @@
 
     public static void onPickSuggestionManually(final SuggestedWords suggestedWords,
             final SuggestedWords.SuggestedWordInfo suggestionInfo) {
+    }
 
+    public static void onBackspaceWordDelete(int wordLength) {
+    }
+
+    public static void onBackspacePressed(int lengthToDelete) {
+    }
+
+    public static void onBackspaceSelectedText(int selectedTextLength) {
+    }
+
+    public static void onDeleteMultiCharInput(int multiCharLength) {
+    }
+
+    public static void onRevertAutoCorrect() {
+    }
+
+    public static void onRevertDoubleSpacePeriod() {
+    }
+
+    public static void onRevertSwapPunctuation() {
     }
 }
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 2e81bdf..f1253b4 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -338,6 +338,8 @@
             <!-- If true, use functionalTextColor instead of ketTextColor to drawing the label on
                  the key -->
             <flag name="followFunctionalTextColor" value="0x80000" />
+            <!-- Keep aspect ratio of key background. -->
+            <flag name="keepBackgroundAspectRatio" value="0x100000" />
             <!-- If true, disable keyHintLabel. -->
             <flag name="disableKeyHintLabel" value="0x40000000" />
             <!-- If true, disable additionalMoreKeys. -->
diff --git a/java/res/values/themes-lxx-dark.xml b/java/res/values/themes-lxx-dark.xml
index 76e9d33..5b26813 100644
--- a/java/res/values/themes-lxx-dark.xml
+++ b/java/res/values/themes-lxx-dark.xml
@@ -114,6 +114,7 @@
         <item name="android:background">@android:color/transparent</item>
         <item name="keyBackground">@drawable/btn_keyboard_key_popup_action_lxx_dark</item>
         <item name="divider">@null</item>
+        <item name="keyLabelFlags">keepBackgroundAspectRatio</item>
     </style>
     <style
         name="SuggestionStripView.LXX_Dark"
diff --git a/java/res/values/themes-lxx-light.xml b/java/res/values/themes-lxx-light.xml
index 5cd8417..f607807 100644
--- a/java/res/values/themes-lxx-light.xml
+++ b/java/res/values/themes-lxx-light.xml
@@ -114,6 +114,7 @@
         <item name="android:background">@android:color/transparent</item>
         <item name="keyBackground">@drawable/btn_keyboard_key_popup_action_lxx_light</item>
         <item name="divider">@null</item>
+        <item name="keyLabelFlags">keepBackgroundAspectRatio</item>
     </style>
     <style
         name="SuggestionStripView.LXX_Light"
diff --git a/java/res/xml-sw600dp/key_styles_enter.xml b/java/res/xml-sw600dp/key_styles_enter.xml
index d066d2d..63ef2f8 100644
--- a/java/res/xml-sw600dp/key_styles_enter.xml
+++ b/java/res/xml-sw600dp/key_styles_enter.xml
@@ -80,13 +80,27 @@
         </default>
     </switch>
     <!-- Enter key style -->
-    <key-style
-        latin:styleName="defaultEnterKeyStyle"
-        latin:keySpec="!icon/enter_key|!code/key_enter"
-        latin:keyLabelFlags="preserveCase|autoXScale|followKeyLabelRatio|followFunctionalTextColor"
-        latin:keyActionFlags="noKeyPreview"
-        latin:backgroundType="action"
-        latin:parentStyle="navigateMoreKeysStyle" />
+    <switch>
+        <case latin:keyboardTheme="ICS|KLP">
+            <key-style
+                latin:styleName="defaultEnterKeyStyle"
+                latin:keySpec="!icon/enter_key|!code/key_enter"
+                latin:keyLabelFlags="preserveCase|autoXScale|followKeyLabelRatio|followFunctionalTextColor"
+                latin:keyActionFlags="noKeyPreview"
+                latin:backgroundType="action"
+                latin:parentStyle="navigateMoreKeysStyle" />
+        </case>
+        <!-- keyboardTheme="LXXLight|LXXDark" -->
+        <default>
+            <key-style
+                latin:styleName="defaultEnterKeyStyle"
+                latin:keySpec="!icon/enter_key|!code/key_enter"
+                latin:keyLabelFlags="preserveCase|autoXScale|followKeyLabelRatio|followFunctionalTextColor|keepBackgroundAspectRatio"
+                latin:keyActionFlags="noKeyPreview"
+                latin:backgroundType="action"
+                latin:parentStyle="navigateMoreKeysStyle" />
+        </default>
+    </switch>
     <include latin:keyboardLayout="@xml/key_styles_actions" />
     <switch>
         <!-- Shift + Enter in textMultiLine field. -->
diff --git a/java/res/xml/key_styles_common.xml b/java/res/xml/key_styles_common.xml
index 43ee26b..b36ddf2 100644
--- a/java/res/xml/key_styles_common.xml
+++ b/java/res/xml/key_styles_common.xml
@@ -80,11 +80,24 @@
         latin:keyActionFlags="isRepeatable|noKeyPreview"
         latin:backgroundType="functional" />
     <!-- emojiKeyStyle must be defined before including @xml/key_syles_enter. -->
-    <key-style
-        latin:styleName="emojiKeyStyle"
-        latin:keySpec="!icon/emoji_action_key|!code/key_emoji"
-        latin:keyActionFlags="noKeyPreview"
-        latin:backgroundType="action" />
+    <switch>
+        <case latin:keyboardTheme="ICS|KLP">
+            <key-style
+                latin:styleName="emojiKeyStyle"
+                latin:keySpec="!icon/emoji_action_key|!code/key_emoji"
+                latin:keyActionFlags="noKeyPreview"
+                latin:backgroundType="action" />
+        </case>
+        <!-- keyboardTheme="LXXLight|LXXDark" -->
+        <default>
+            <key-style
+                latin:styleName="emojiKeyStyle"
+                latin:keySpec="!icon/emoji_action_key|!code/key_emoji"
+                latin:keyLabelFlags="keepBackgroundAspectRatio"
+                latin:keyActionFlags="noKeyPreview"
+                latin:backgroundType="action" />
+        </default>
+    </switch>
     <include
         latin:keyboardLayout="@xml/key_styles_enter" />
     <!-- TODO: Currently there is no way to specify icon alignment per theme. -->
diff --git a/java/res/xml/key_styles_enter.xml b/java/res/xml/key_styles_enter.xml
index d6d01b8..564f465 100644
--- a/java/res/xml/key_styles_enter.xml
+++ b/java/res/xml/key_styles_enter.xml
@@ -212,13 +212,27 @@
         </default>
     </switch>
     <!-- Enter key style -->
-    <key-style
-        latin:styleName="defaultEnterKeyStyle"
-        latin:keySpec="!icon/enter_key|!code/key_enter"
-        latin:keyLabelFlags="preserveCase|autoXScale|followKeyLabelRatio|followFunctionalTextColor"
-        latin:keyActionFlags="noKeyPreview"
-        latin:backgroundType="action"
-        latin:parentStyle="navigateMoreKeysStyle" />
+    <switch>
+        <case latin:keyboardTheme="ICS|KLP">
+            <key-style
+                latin:styleName="defaultEnterKeyStyle"
+                latin:keySpec="!icon/enter_key|!code/key_enter"
+                latin:keyLabelFlags="preserveCase|autoXScale|followKeyLabelRatio|followFunctionalTextColor"
+                latin:keyActionFlags="noKeyPreview"
+                latin:backgroundType="action"
+                latin:parentStyle="navigateMoreKeysStyle" />
+        </case>
+        <!-- keyboardTheme="LXXLight|LXXDark" -->
+        <default>
+            <key-style
+                latin:styleName="defaultEnterKeyStyle"
+                latin:keySpec="!icon/enter_key|!code/key_enter"
+                latin:keyLabelFlags="preserveCase|autoXScale|followKeyLabelRatio|followFunctionalTextColor|keepBackgroundAspectRatio"
+                latin:keyActionFlags="noKeyPreview"
+                latin:backgroundType="action"
+                latin:parentStyle="navigateMoreKeysStyle" />
+        </default>
+    </switch>
     <include latin:keyboardLayout="@xml/key_styles_actions" />
     <switch>
         <!-- Shift + Enter in textMultiLine field. -->
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index a6f9f3c..bd1c147 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -87,6 +87,7 @@
     private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000;
     private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000;
     private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000;
+    private static final int LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO = 0x100000;
     private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000;
     private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000;
 
@@ -697,6 +698,10 @@
         return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE;
     }
 
+    public final boolean needsToKeepBackgroundAspectRatio(final int defaultFlags) {
+        return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO) != 0;
+    }
+
     private final boolean isShiftedLetterActivated() {
         return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0
                 && !TextUtils.isEmpty(mHintLabel);
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 075cd90..bb3cbb0 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -340,11 +340,25 @@
     // Draw key background.
     protected void onDrawKeyBackground(final Key key, final Canvas canvas,
             final Drawable background) {
-        final Rect padding = mKeyBackgroundPadding;
-        final int bgWidth = key.getDrawWidth() + padding.left + padding.right;
-        final int bgHeight = key.getHeight() + padding.top + padding.bottom;
-        final int bgX = -padding.left;
-        final int bgY = -padding.top;
+        final int keyWidth = key.getDrawWidth();
+        final int keyHeight = key.getHeight();
+        final int bgWidth, bgHeight, bgX, bgY;
+        if (key.needsToKeepBackgroundAspectRatio(mDefaultKeyLabelFlags)) {
+            final int intrinsicWidth = background.getIntrinsicWidth();
+            final int intrinsicHeight = background.getIntrinsicHeight();
+            final float minScale = Math.min(
+                    keyWidth / (float)intrinsicWidth, keyHeight / (float)intrinsicHeight);
+            bgWidth = (int)(intrinsicWidth * minScale);
+            bgHeight = (int)(intrinsicHeight * minScale);
+            bgX = (keyWidth - bgWidth) / 2;
+            bgY = (keyHeight - bgHeight) / 2;
+        } else {
+            final Rect padding = mKeyBackgroundPadding;
+            bgWidth = keyWidth + padding.left + padding.right;
+            bgHeight = keyHeight + padding.top + padding.bottom;
+            bgX = -padding.left;
+            bgY = -padding.top;
+        }
         final Rect bounds = background.getBounds();
         if (bgWidth != bounds.right || bgHeight != bounds.bottom) {
             background.setBounds(0, 0, bgWidth, bgHeight);
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
index 73c84cd..abcfff8 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -308,8 +308,8 @@
                 dividerWidth = 0;
             }
             final MoreKeySpec[] moreKeys = key.getMoreKeys();
-            mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyWidth, rowHeight,
-                    key.getX() + key.getWidth() / 2, keyboard.mId.mWidth,
+            mParams.setParameters(moreKeys.length, key.getMoreKeysColumnNumber(), keyWidth,
+                    rowHeight, key.getX() + key.getWidth() / 2, keyboard.mId.mWidth,
                     key.isMoreKeysFixedColumn(), key.isMoreKeysFixedOrder(), dividerWidth);
         }
 
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index fde94da..4642883 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -24,6 +24,7 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback;
 import com.android.inputmethod.latin.PrevWordsInfo.WordInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.personalization.ContextualDictionary;
@@ -36,7 +37,6 @@
 import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions;
 import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary;
 import com.android.inputmethod.latin.utils.ExecutorUtils;
-import com.android.inputmethod.latin.utils.LanguageModelParam;
 import com.android.inputmethod.latin.utils.SuggestionResults;
 
 import java.io.File;
@@ -60,7 +60,6 @@
     // HACK: This threshold is being used when adding a capitalized entry in the User History
     // dictionary.
     private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140;
-    private static final int MAX_DICTIONARY_FACILITATOR_CACHE_SIZE = 3;
 
     private DictionaryGroup mDictionaryGroup = new DictionaryGroup();
     private boolean mIsUserDictEnabled = false;
@@ -68,7 +67,7 @@
     // To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
     private final Object mLock = new Object();
     private final DistracterFilter mDistracterFilter;
-    private final DictionaryFacilitatorLruCache mFacilitatorCacheForPersonalization;
+    private final PersonalizationDictionaryFacilitator mPersonalizationDictionaryFacilitator;
 
     private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS =
             new String[] {
@@ -176,18 +175,22 @@
 
     public DictionaryFacilitator() {
         mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER;
-        mFacilitatorCacheForPersonalization = null;
+        mPersonalizationDictionaryFacilitator = null;
     }
 
     public DictionaryFacilitator(final Context context) {
-        mFacilitatorCacheForPersonalization = new DictionaryFacilitatorLruCache(context,
-                MAX_DICTIONARY_FACILITATOR_CACHE_SIZE, "" /* dictionaryNamePrefix */);
-        mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context,
-                mFacilitatorCacheForPersonalization);
+        mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context);
+        mPersonalizationDictionaryFacilitator =
+                new PersonalizationDictionaryFacilitator(context, mDistracterFilter);
     }
 
     public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) {
         mDistracterFilter.updateEnabledSubtypes(enabledSubtypes);
+        mPersonalizationDictionaryFacilitator.updateEnabledSubtypes(enabledSubtypes);
+    }
+
+    public void setIsMonolingualUser(final boolean isMonolingualUser) {
+        mPersonalizationDictionaryFacilitator.setIsMonolingualUser(isMonolingualUser);
     }
 
     public Locale getLocale() {
@@ -358,10 +361,10 @@
         for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) {
             dictionaryGroup.closeDict(dictType);
         }
-        if (mFacilitatorCacheForPersonalization != null) {
-            mFacilitatorCacheForPersonalization.evictAll();
-        }
         mDistracterFilter.close();
+        if (mPersonalizationDictionaryFacilitator != null) {
+            mPersonalizationDictionaryFacilitator.close();
+        }
     }
 
     @UsedForTesting
@@ -381,11 +384,11 @@
     }
 
     public void flushPersonalizationDictionary() {
-        final ExpandableBinaryDictionary personalizationDict =
+        final ExpandableBinaryDictionary personalizationDictUsedForSuggestion =
                 mDictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION);
-        if (personalizationDict != null) {
-            personalizationDict.asyncFlushBinaryDictionary();
-        }
+        mPersonalizationDictionaryFacilitator.flushPersonalizationDictionariesToUpdate(
+                personalizationDictUsedForSuggestion);
+        mDistracterFilter.close();
     }
 
     public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit)
@@ -589,6 +592,7 @@
     // personalization dictionary.
     public void clearPersonalizationDictionary() {
         clearSubDictionary(Dictionary.TYPE_PERSONALIZATION);
+        mPersonalizationDictionaryFacilitator.clearDictionariesToUpdate();
     }
 
     public void clearContextualDictionary() {
@@ -598,33 +602,9 @@
     public void addEntriesToPersonalizationDictionary(
             final PersonalizationDataChunk personalizationDataChunk,
             final SpacingAndPunctuations spacingAndPunctuations,
-            final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
-        final ExpandableBinaryDictionary personalizationDict =
-                mDictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION);
-        if (personalizationDict == null) {
-            if (callback != null) {
-                callback.onFinished();
-            }
-            return;
-        }
-        // TODO: Get locale from personalizationDataChunk.mDetectedLanguage.
-        final Locale dataChunkLocale = getLocale();
-        final DictionaryFacilitator dictionaryFacilitatorForLocale =
-                mFacilitatorCacheForPersonalization.get(dataChunkLocale);
-        final ArrayList<LanguageModelParam> languageModelParams =
-                LanguageModelParam.createLanguageModelParamsFrom(
-                        personalizationDataChunk.mTokens,
-                        personalizationDataChunk.mTimestampInSeconds,
-                        dictionaryFacilitatorForLocale, spacingAndPunctuations,
-                        new DistracterFilterCheckingIsInDictionary(
-                                mDistracterFilter, personalizationDict));
-        if (languageModelParams == null || languageModelParams.isEmpty()) {
-            if (callback != null) {
-                callback.onFinished();
-            }
-            return;
-        }
-        personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback);
+            final AddMultipleDictionaryEntriesCallback callback) {
+        mPersonalizationDictionaryFacilitator.addEntriesToPersonalizationDictionariesToUpdate(
+                getLocale(), personalizationDataChunk, spacingAndPunctuations, callback);
     }
 
     public void addPhraseToContextualDictionary(final String[] phrase, final int probability,
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 67e2ca5..d210499 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -614,9 +614,10 @@
 
     private void refreshPersonalizationDictionarySession(
             final SettingsValues currentSettingsValues) {
-        mPersonalizationDictionaryUpdater.onLoadSettings(
-                currentSettingsValues.mUsePersonalizedDicts,
+        mDictionaryFacilitator.setIsMonolingualUser(
                 mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes());
+        mPersonalizationDictionaryUpdater.onLoadSettings(
+                currentSettingsValues.mUsePersonalizedDicts);
         mContextualDictionaryUpdater.onLoadSettings(currentSettingsValues.mUsePersonalizedDicts);
         final boolean shouldKeepUserHistoryDictionaries;
         if (currentSettingsValues.mUsePersonalizedDicts) {
@@ -734,10 +735,6 @@
                 cleanupInternalStateForFinishInput();
             }
         }
-        // TODO: Remove this test.
-        if (!conf.locale.equals(mPersonalizationDictionaryUpdater.getLocale())) {
-            refreshPersonalizationDictionarySession(settingsValues);
-        }
         super.onConfigurationChanged(conf);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/PersonalizationDictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/PersonalizationDictionaryFacilitator.java
new file mode 100644
index 0000000..aa8e312
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/PersonalizationDictionaryFacilitator.java
@@ -0,0 +1,185 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import android.content.Context;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback;
+import com.android.inputmethod.latin.personalization.PersonalizationDataChunk;
+import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+import com.android.inputmethod.latin.utils.DistracterFilter;
+import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary;
+import com.android.inputmethod.latin.utils.LanguageModelParam;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+
+/**
+ * Class for managing and updating personalization dictionaries.
+ */
+public class PersonalizationDictionaryFacilitator {
+    private final Context mContext;
+    private final DistracterFilter mDistracterFilter;
+    private final HashMap<String, HashSet<Locale>> mLangToLocalesMap = new HashMap<>();
+    private final HashMap<Locale, ExpandableBinaryDictionary> mPersonalizationDictsToUpdate =
+            new HashMap<>();
+    private boolean mIsMonolingualUser = false;;
+
+    PersonalizationDictionaryFacilitator(final Context context,
+            final DistracterFilter distracterFilter) {
+        mContext = context;
+        mDistracterFilter = distracterFilter;
+    }
+
+    public void close() {
+        mLangToLocalesMap.clear();
+        for (final ExpandableBinaryDictionary dict : mPersonalizationDictsToUpdate.values()) {
+            dict.close();
+        }
+        mPersonalizationDictsToUpdate.clear();
+    }
+
+    public void clearDictionariesToUpdate() {
+        for (final ExpandableBinaryDictionary dict : mPersonalizationDictsToUpdate.values()) {
+            dict.clear();
+        }
+        mPersonalizationDictsToUpdate.clear();
+    }
+
+    public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) {
+        for (final InputMethodSubtype subtype : enabledSubtypes) {
+            final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype);
+            final String language = locale.getLanguage();
+            final HashSet<Locale> locales = mLangToLocalesMap.get(language);
+            if (locales != null) {
+                locales.add(locale);
+            } else {
+                final HashSet<Locale> localeSet = new HashSet<>();
+                localeSet.add(locale);
+                mLangToLocalesMap.put(language, localeSet);
+            }
+        }
+    }
+
+    public void setIsMonolingualUser(final boolean isMonolingualUser) {
+        mIsMonolingualUser = isMonolingualUser;
+    }
+
+    /**
+     * Flush personalization dictionaries to dictionary files. Close dictionaries after writing
+     * files except the dictionary that is used for generating suggestions.
+     *
+     * @param personalizationDictUsedForSuggestion the personalization dictionary used for
+     * generating suggestions that won't be closed.
+     */
+    public void flushPersonalizationDictionariesToUpdate(
+            final ExpandableBinaryDictionary personalizationDictUsedForSuggestion) {
+        for (final ExpandableBinaryDictionary personalizationDict :
+                mPersonalizationDictsToUpdate.values()) {
+            personalizationDict.asyncFlushBinaryDictionary();
+            if (personalizationDict != personalizationDictUsedForSuggestion) {
+                // Close if the dictionary is not being used for suggestion.
+                personalizationDict.close();
+            }
+        }
+        mDistracterFilter.close();
+        mPersonalizationDictsToUpdate.clear();
+    }
+
+    private ExpandableBinaryDictionary getPersonalizationDictToUpdate(final Context context,
+            final Locale locale) {
+        ExpandableBinaryDictionary personalizationDict = mPersonalizationDictsToUpdate.get(locale);
+        if (personalizationDict != null) {
+            return personalizationDict;
+        }
+        personalizationDict = PersonalizationDictionary.getDictionary(context, locale,
+                null /* dictFile */, "" /* dictNamePrefix */);
+        mPersonalizationDictsToUpdate.put(locale, personalizationDict);
+        return personalizationDict;
+    }
+
+    private void addEntriesToPersonalizationDictionariesForLocale(final Locale locale,
+            final PersonalizationDataChunk personalizationDataChunk,
+            final SpacingAndPunctuations spacingAndPunctuations,
+            final AddMultipleDictionaryEntriesCallback callback) {
+        final ExpandableBinaryDictionary personalizationDict =
+                getPersonalizationDictToUpdate(mContext, locale);
+        if (personalizationDict == null) {
+            if (callback != null) {
+                callback.onFinished();
+            }
+            return;
+        }
+        final ArrayList<LanguageModelParam> languageModelParams =
+                LanguageModelParam.createLanguageModelParamsFrom(
+                        personalizationDataChunk.mTokens,
+                        personalizationDataChunk.mTimestampInSeconds, spacingAndPunctuations,
+                        locale, new DistracterFilterCheckingIsInDictionary(
+                                mDistracterFilter, personalizationDict));
+        if (languageModelParams == null || languageModelParams.isEmpty()) {
+            if (callback != null) {
+                callback.onFinished();
+            }
+            return;
+        }
+        personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback);
+    }
+
+    public void addEntriesToPersonalizationDictionariesToUpdate(final Locale defaultLocale,
+            final PersonalizationDataChunk personalizationDataChunk,
+            final SpacingAndPunctuations spacingAndPunctuations,
+            final AddMultipleDictionaryEntriesCallback callback) {
+        final String language = personalizationDataChunk.mDetectedLanguage;
+        final HashSet<Locale> locales;
+        if (mIsMonolingualUser && PersonalizationDataChunk.LANGUAGE_UNKNOWN.equals(language)
+                && mLangToLocalesMap.size() == 1) {
+            locales = mLangToLocalesMap.get(defaultLocale.getLanguage());
+        } else {
+            locales = mLangToLocalesMap.get(language);
+        }
+        if (locales == null || locales.isEmpty()) {
+            if (callback != null) {
+                callback.onFinished();
+            }
+            return;
+        }
+        final AtomicInteger remainingTaskCount = new AtomicInteger(locales.size());
+        final AddMultipleDictionaryEntriesCallback callbackForLocales =
+                new AddMultipleDictionaryEntriesCallback() {
+                    @Override
+                    public void onFinished() {
+                        if (remainingTaskCount.decrementAndGet() == 0) {
+                            // Update tasks for all locales have been finished.
+                            if (callback != null) {
+                                callback.onFinished();
+                            }
+                        }
+                    }
+                };
+        for (final Locale locale : locales) {
+            addEntriesToPersonalizationDictionariesForLocale(locale, personalizationDataChunk,
+                    spacingAndPunctuations, callbackForLocales);
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 26acabd..c5e60d6 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -1088,8 +1088,10 @@
                 if (!TextUtils.isEmpty(rejectedSuggestion)) {
                     mDictionaryFacilitator.removeWordFromPersonalizedDicts(rejectedSuggestion);
                 }
+                StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length());
             } else {
                 mWordComposer.applyProcessedEvent(event);
+                StatsUtils.onBackspacePressed(1);
             }
             if (mWordComposer.isComposingWord()) {
                 setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
@@ -1100,6 +1102,7 @@
         } else {
             if (mLastComposedWord.canRevertCommit()) {
                 revertCommit(inputTransaction);
+                StatsUtils.onRevertAutoCorrect();
                 return;
             }
             if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
@@ -1107,6 +1110,7 @@
                 // This is triggered on backspace after a key that inputs multiple characters,
                 // like the smiley key or the .com key.
                 mConnection.deleteSurroundingText(mEnteredText.length(), 0);
+                StatsUtils.onDeleteMultiCharInput(mEnteredText.length());
                 mEnteredText = null;
                 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
                 // In addition we know that spaceState is false, and that we should not be
@@ -1122,10 +1126,12 @@
                     inputTransaction.setRequiresUpdateSuggestions();
                     mWordComposer.setCapitalizedModeAtStartComposingTime(
                             WordComposer.CAPS_MODE_OFF);
+                    StatsUtils.onRevertDoubleSpacePeriod();
                     return;
                 }
             } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
                 if (mConnection.revertSwapPunctuation()) {
+                    StatsUtils.onRevertSwapPunctuation();
                     // Likewise
                     return;
                 }
@@ -1140,6 +1146,7 @@
                 mConnection.setSelection(mConnection.getExpectedSelectionEnd(),
                         mConnection.getExpectedSelectionEnd());
                 mConnection.deleteSurroundingText(numCharsDeleted, 0);
+                StatsUtils.onBackspaceSelectedText(numCharsDeleted);
             } else {
                 // There is no selection, just delete one character.
                 if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) {
@@ -1156,9 +1163,12 @@
                     // applications are relying on this behavior so we continue to support it for
                     // older apps, so we retain this behavior if the app has target SDK < JellyBean.
                     sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
+                    int totalDeletedLength = 1;
                     if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
                         sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
+                        totalDeletedLength++;
                     }
+                    StatsUtils.onBackspacePressed(totalDeletedLength);
                 } else {
                     final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
                     if (codePointBeforeCursor == Constants.NOT_A_CODE) {
@@ -1169,11 +1179,13 @@
                         // catch it and have their broken interface react. If you need the keyboard
                         // to do this, you're doing it wrong -- please fix your app.
                         mConnection.deleteSurroundingText(1, 0);
+                        // TODO: Add a new StatsUtils method onBackspaceWhenNoText()
                         return;
                     }
                     final int lengthToDelete =
                             Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
                     mConnection.deleteSurroundingText(lengthToDelete, 0);
+                    int totalDeletedLength = lengthToDelete;
                     if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
                         final int codePointBeforeCursorToDeleteAgain =
                                 mConnection.getCodePointBeforeCursor();
@@ -1181,8 +1193,10 @@
                             final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
                                     codePointBeforeCursorToDeleteAgain) ? 2 : 1;
                             mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
+                            totalDeletedLength += lengthToDeleteAgain;
                         }
                     }
+                    StatsUtils.onBackspacePressed(totalDeletedLength);
                 }
             }
             if (inputTransaction.mSettingsValues
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java
index 6f4b097..734ed55 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java
@@ -20,6 +20,8 @@
 import java.util.List;
 
 public class PersonalizationDataChunk {
+    public static final String LANGUAGE_UNKNOWN = "";
+
     public final boolean mInputByUser;
     public final List<String> mTokens;
     public final int mTimestampInSeconds;
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
index 787e4a5..94c6242 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
@@ -36,10 +36,38 @@
     public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo,
             final String testedWord, final Locale locale);
 
+    public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, final String testedWord,
+            final Locale locale);
+
     public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes);
 
     public void close();
 
+    public static final class HandlingType {
+        private final static int REQUIRE_NO_SPECIAL_HANDLINGS = 0x0;
+        private final static int SHOULD_BE_LOWER_CASED = 0x1;
+        private final static int SHOULD_BE_HANDLED_AS_OOV = 0x2;
+
+        public static int getHandlingType(final boolean shouldBeLowerCased, final boolean isOov) {
+            int wordHandlingType = HandlingType.REQUIRE_NO_SPECIAL_HANDLINGS;
+            if (shouldBeLowerCased) {
+                wordHandlingType |= HandlingType.SHOULD_BE_LOWER_CASED;
+            }
+            if (isOov) {
+                wordHandlingType |= HandlingType.SHOULD_BE_HANDLED_AS_OOV;
+            }
+            return wordHandlingType;
+        }
+
+        public static boolean shouldBeLowerCased(final int handlingType) {
+            return (handlingType & SHOULD_BE_LOWER_CASED) != 0;
+        }
+
+        public static boolean shouldBeHandledAsOov(final int handlingType) {
+            return (handlingType & SHOULD_BE_HANDLED_AS_OOV) != 0;
+        }
+    };
+
     public static final DistracterFilter EMPTY_DISTRACTER_FILTER = new DistracterFilter() {
         @Override
         public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo,
@@ -48,6 +76,12 @@
         }
 
         @Override
+        public int getWordHandlingType(final PrevWordsInfo prevWordsInfo,
+                final String testedWord, final Locale locale) {
+            return HandlingType.REQUIRE_NO_SPECIAL_HANDLINGS;
+        }
+
+        @Override
         public void close() {
         }
 
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
index e10571e..1db5255 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
@@ -51,6 +51,7 @@
             DistracterFilterCheckingExactMatchesAndSuggestions.class.getSimpleName();
     private static final boolean DEBUG = false;
 
+    private static final int MAX_DICTIONARY_FACILITATOR_CACHE_SIZE = 3;
     private static final int MAX_DISTRACTERS_CACHE_SIZE = 1024;
 
     private final Context mContext;
@@ -73,15 +74,13 @@
      * Create a DistracterFilter instance.
      *
      * @param context the context.
-     * @param dictionaryFacilitatorLruCache the cache of dictionaryFacilitators that are used for
-     * checking distracters.
      */
-    public DistracterFilterCheckingExactMatchesAndSuggestions(final Context context,
-            final DictionaryFacilitatorLruCache dictionaryFacilitatorLruCache) {
+    public DistracterFilterCheckingExactMatchesAndSuggestions(final Context context) {
         mContext = context;
         mLocaleToSubtypeCache = new ConcurrentHashMap<>();
         mLocaleToKeyboardCache = new ConcurrentHashMap<>();
-        mDictionaryFacilitatorLruCache = dictionaryFacilitatorLruCache;
+        mDictionaryFacilitatorLruCache = new DictionaryFacilitatorLruCache(context,
+                MAX_DICTIONARY_FACILITATOR_CACHE_SIZE, "" /* dictionaryNamePrefix */);
         mDistractersCache = new LruCache<>(MAX_DISTRACTERS_CACHE_SIZE);
     }
 
@@ -89,7 +88,8 @@
     public void close() {
         mLocaleToSubtypeCache.clear();
         mLocaleToKeyboardCache.clear();
-        mDistractersCache.evictAll();
+        mDictionaryFacilitatorLruCache.evictAll();
+        // Don't clear mDistractersCache.
     }
 
     @Override
@@ -194,9 +194,8 @@
             mDistractersCache.put(cacheKey, Boolean.TRUE);
             return true;
         }
-        final boolean isValidWord = dictionaryFacilitator.isValidWord(testedWord,
-                false /* ignoreCase */);
-        if (isValidWord) {
+        final boolean Word = dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */);
+        if (Word) {
             // Valid word is not a distractor.
             if (DEBUG) {
                 Log.d(TAG, "isDistracter: false (valid word)");
@@ -283,4 +282,41 @@
         }
         return false;
     }
+
+    private boolean shouldBeLowerCased(final PrevWordsInfo prevWordsInfo, final String testedWord,
+            final Locale locale) {
+        final DictionaryFacilitator dictionaryFacilitator =
+                mDictionaryFacilitatorLruCache.get(locale);
+        if (dictionaryFacilitator.isValidWord(testedWord, false /* ignoreCase */)) {
+            return false;
+        }
+        final String lowerCaseTargetWord = testedWord.toLowerCase(locale);
+        if (testedWord.equals(lowerCaseTargetWord)) {
+            return false;
+        }
+        if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) {
+            return true;
+        }
+        if (StringUtils.getCapitalizationType(testedWord) == StringUtils.CAPITALIZE_FIRST
+                && !prevWordsInfo.isValid()) {
+            // TODO: Check beginning-of-sentence.
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, final String testedWord,
+            final Locale locale) {
+        // TODO: Use this method for user history dictionary.
+        if (testedWord == null|| locale == null) {
+            return HandlingType.getHandlingType(false /* shouldBeLowerCased */, false /* isOov */);
+        }
+        final boolean shouldBeLowerCased = shouldBeLowerCased(prevWordsInfo, testedWord, locale);
+        final String caseModifiedWord =
+                shouldBeLowerCased ? testedWord.toLowerCase(locale) : testedWord;
+        final boolean isOov = !mDictionaryFacilitatorLruCache.get(locale).isValidWord(
+                caseModifiedWord, false /* ignoreCase */);
+        return HandlingType.getHandlingType(shouldBeLowerCased, isOov);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java
index 4ad4ba7..349236f 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingIsInDictionary.java
@@ -48,6 +48,12 @@
     }
 
     @Override
+    public int getWordHandlingType(final PrevWordsInfo prevWordsInfo, final String testedWord,
+            final Locale locale) {
+        return mDistracterFilter.getWordHandlingType(prevWordsInfo, testedWord, locale);
+    }
+
+    @Override
     public void updateEnabledSubtypes(List<InputMethodSubtype> enabledSubtypes) {
         // Do nothing.
     }
diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
index fbce3f2..05d1247 100644
--- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
+++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.latin.DictionaryFacilitator;
 import com.android.inputmethod.latin.PrevWordsInfo;
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
+import com.android.inputmethod.latin.utils.DistracterFilter.HandlingType;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -81,8 +82,7 @@
     // Process a list of words and return a list of {@link LanguageModelParam} objects.
     public static ArrayList<LanguageModelParam> createLanguageModelParamsFrom(
             final List<String> tokens, final int timestamp,
-            final DictionaryFacilitator dictionaryFacilitator,
-            final SpacingAndPunctuations spacingAndPunctuations,
+            final SpacingAndPunctuations spacingAndPunctuations, final Locale locale,
             final DistracterFilter distracterFilter) {
         final ArrayList<LanguageModelParam> languageModelParams = new ArrayList<>();
         final int N = tokens.size();
@@ -111,8 +111,7 @@
             }
             final LanguageModelParam languageModelParam =
                     detectWhetherVaildWordOrNotAndGetLanguageModelParam(
-                            prevWordsInfo, tempWord, timestamp, dictionaryFacilitator,
-                            distracterFilter);
+                            prevWordsInfo, tempWord, timestamp, locale, distracterFilter);
             if (languageModelParam == null) {
                 continue;
             }
@@ -125,47 +124,25 @@
 
     private static LanguageModelParam detectWhetherVaildWordOrNotAndGetLanguageModelParam(
             final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp,
-            final DictionaryFacilitator dictionaryFacilitator,
-            final DistracterFilter distracterFilter) {
-        final Locale locale = dictionaryFacilitator.getLocale();
+            final Locale locale, final DistracterFilter distracterFilter) {
         if (locale == null) {
             return null;
         }
-        if (dictionaryFacilitator.isValidWord(targetWord, false /* ignoreCase */)) {
-            return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp,
-                    true /* isValidWord */, locale, distracterFilter);
+        final int wordHandlingType = distracterFilter.getWordHandlingType(prevWordsInfo,
+                targetWord, locale);
+        final String word = HandlingType.shouldBeLowerCased(wordHandlingType) ?
+                targetWord.toLowerCase(locale) : targetWord;
+        if (distracterFilter.isDistracterToWordsInDictionaries(prevWordsInfo, targetWord, locale)) {
+            // The word is a distracter.
+            return null;
         }
-
-        final String lowerCaseTargetWord = targetWord.toLowerCase(locale);
-        if (dictionaryFacilitator.isValidWord(lowerCaseTargetWord, false /* ignoreCase */)) {
-            // Add the lower-cased word.
-            return createAndGetLanguageModelParamOfWord(prevWordsInfo, lowerCaseTargetWord,
-                    timestamp, true /* isValidWord */, locale, distracterFilter);
-        }
-
-        // Treat the word as an OOV word.
-        return createAndGetLanguageModelParamOfWord(prevWordsInfo, targetWord, timestamp,
-                false /* isValidWord */, locale, distracterFilter);
+        return createAndGetLanguageModelParamOfWord(prevWordsInfo, word, timestamp,
+                !HandlingType.shouldBeHandledAsOov(wordHandlingType));
     }
 
     private static LanguageModelParam createAndGetLanguageModelParamOfWord(
-            final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp,
-            final boolean isValidWord, final Locale locale,
-            final DistracterFilter distracterFilter) {
-        final String word;
-        if (StringUtils.getCapitalizationType(targetWord) == StringUtils.CAPITALIZE_FIRST
-                && !prevWordsInfo.isValid() && !isValidWord) {
-            word = targetWord.toLowerCase(locale);
-        } else {
-            word = targetWord;
-        }
-        // Check whether the word is a distracter to words in the dictionaries.
-        if (distracterFilter.isDistracterToWordsInDictionaries(prevWordsInfo, word, locale)) {
-            if (DEBUG) {
-                Log.d(TAG, "The word (" + word + ") is a distracter. Skip this word.");
-            }
-            return null;
-        }
+            final PrevWordsInfo prevWordsInfo, final String word, final int timestamp,
+            final boolean isValidWord) {
         final int unigramProbability = isValidWord ?
                 UNIGRAM_PROBABILITY_FOR_VALID_WORD : UNIGRAM_PROBABILITY_FOR_OOV_WORD;
         if (!prevWordsInfo.isValid()) {
diff --git a/tests/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryTests.java
index e9a97ff..4e7e814 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/PersonalizationDictionaryTests.java
@@ -29,6 +29,7 @@
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.DictionaryFacilitator;
 import com.android.inputmethod.latin.ExpandableBinaryDictionary;
+import com.android.inputmethod.latin.RichInputMethodManager;
 import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback;
 import com.android.inputmethod.latin.makedict.CodePointUtils;
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
@@ -36,6 +37,7 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
+import android.view.inputmethod.InputMethodSubtype;
 
 /**
  * Unit tests for personalization dictionary
@@ -55,16 +57,28 @@
         final DictionaryFacilitator dictionaryFacilitator = new DictionaryFacilitator(getContext());
         dictionaryFacilitator.resetDictionariesForTesting(getContext(), LOCALE_EN_US, dictTypes,
                 new HashMap<String, File>(), new HashMap<String, Map<String, String>>());
+        // Set subtypes.
+        RichInputMethodManager.init(getContext());
+        final RichInputMethodManager richImm = RichInputMethodManager.getInstance();
+        final ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
+        subtypes.add(richImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                LOCALE_EN_US.toString(), "qwerty"));
+        dictionaryFacilitator.updateEnabledSubtypes(subtypes);
         return dictionaryFacilitator;
     }
 
     public void testAddManyTokens() {
         final DictionaryFacilitator dictionaryFacilitator = getDictionaryFacilitator();
         dictionaryFacilitator.clearPersonalizationDictionary();
-        final int dataChunkCount = 20;
-        final int wordCountInOneChunk = 2000;
+        final int dataChunkCount = 100;
+        final int wordCountInOneChunk = 200;
+        final int uniqueWordCount = 100;
         final Random random = new Random(System.currentTimeMillis());
         final int[] codePointSet = CodePointUtils.LATIN_ALPHABETS_LOWER;
+        final ArrayList<String> words = new ArrayList<>();
+        for (int i = 0; i < uniqueWordCount; i++) {
+            words.add(CodePointUtils.generateWord(random, codePointSet));
+        }
 
         final SpacingAndPunctuations spacingAndPunctuations =
                 new SpacingAndPunctuations(getContext().getResources());
@@ -75,7 +89,7 @@
         for (int i = 0; i < dataChunkCount; i++) {
             final ArrayList<String> tokens = new ArrayList<>();
             for (int j = 0; j < wordCountInOneChunk; j++) {
-                tokens.add(CodePointUtils.generateWord(random, codePointSet));
+                tokens.add(words.get(random.nextInt(words.size())));
             }
             final PersonalizationDataChunk personalizationDataChunk = new PersonalizationDataChunk(
                     true /* inputByUser */, tokens, timeStampInSeconds, DUMMY_PACKAGE_NAME,
diff --git a/tests/src/com/android/inputmethod/latin/DistracterFilterTest.java b/tests/src/com/android/inputmethod/latin/utils/DistracterFilterTest.java
similarity index 87%
rename from tests/src/com/android/inputmethod/latin/DistracterFilterTest.java
rename to tests/src/com/android/inputmethod/latin/utils/DistracterFilterTest.java
index af22fb8..5fbd36a 100644
--- a/tests/src/com/android/inputmethod/latin/DistracterFilterTest.java
+++ b/tests/src/com/android/inputmethod/latin/utils/DistracterFilterTest.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
 
 import java.util.ArrayList;
 import java.util.Locale;
@@ -24,24 +24,22 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.view.inputmethod.InputMethodSubtype;
 
-import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions;
+import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.RichInputMethodManager;
+import com.android.inputmethod.latin.utils.DistracterFilter.HandlingType;
 
 /**
  * Unit test for DistracterFilter
  */
 @LargeTest
 public class DistracterFilterTest extends AndroidTestCase {
-    private DictionaryFacilitatorLruCache mDictionaryFacilitatorLruCache;
     private DistracterFilterCheckingExactMatchesAndSuggestions mDistracterFilter;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
         final Context context = getContext();
-        mDictionaryFacilitatorLruCache = new DictionaryFacilitatorLruCache(context,
-                2 /* maxSize */, "" /* dictionaryNamePrefix */);
-        mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context,
-                mDictionaryFacilitatorLruCache);
+        mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context);
         RichInputMethodManager.init(context);
         final RichInputMethodManager richImm = RichInputMethodManager.getInstance();
         final ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
@@ -56,7 +54,7 @@
 
     @Override
     protected void tearDown() {
-        mDictionaryFacilitatorLruCache.evictAll();
+        mDistracterFilter.close();
     }
 
     public void testIsDistractorToWordsInDictionaries() {
@@ -203,4 +201,25 @@
         assertTrue(mDistracterFilter.isDistracterToWordsInDictionaries(
                 EMPTY_PREV_WORDS_INFO, typedWord, localeFrFr));
     }
+
+    public void testGetWordHandlingType() {
+        final Locale localeEnUs = new Locale("en", "US");
+        final PrevWordsInfo EMPTY_PREV_WORDS_INFO = PrevWordsInfo.EMPTY_PREV_WORDS_INFO;
+        int handlingType = 0;
+
+        handlingType = mDistracterFilter.getWordHandlingType(EMPTY_PREV_WORDS_INFO,
+                "this", localeEnUs);
+        assertFalse(HandlingType.shouldBeLowerCased(handlingType));
+        assertFalse(HandlingType.shouldBeHandledAsOov(handlingType));
+
+        handlingType = mDistracterFilter.getWordHandlingType(EMPTY_PREV_WORDS_INFO,
+                "This", localeEnUs);
+        assertTrue(HandlingType.shouldBeLowerCased(handlingType));
+        assertFalse(HandlingType.shouldBeHandledAsOov(handlingType));
+
+        handlingType = mDistracterFilter.getWordHandlingType(EMPTY_PREV_WORDS_INFO,
+                "thibk", localeEnUs);
+        assertFalse(HandlingType.shouldBeLowerCased(handlingType));
+        assertTrue(HandlingType.shouldBeHandledAsOov(handlingType));
+    }
 }