Merge "Support Beginning-of-Sentence in native code"
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index 668eb92..743bc80 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -47,7 +47,7 @@
     // used to identify the versions for upgrades. This should never change going forward.
     private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6;
     // The current database version.
-    private static final int CURRENT_METADATA_DATABASE_VERSION = 8;
+    private static final int CURRENT_METADATA_DATABASE_VERSION = 9;
 
     private final static long NOT_A_DOWNLOAD_ID = -1;
 
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 67ca595..efc5a61 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -192,7 +192,6 @@
     public static final int CODE_SPACE = ' ';
     public static final int CODE_PERIOD = '.';
     public static final int CODE_COMMA = ',';
-    public static final int CODE_ARMENIAN_PERIOD = 0x0589;
     public static final int CODE_DASH = '-';
     public static final int CODE_SINGLE_QUOTE = '\'';
     public static final int CODE_DOUBLE_QUOTE = '"';
@@ -208,6 +207,8 @@
     public static final int CODE_CLOSING_SQUARE_BRACKET = ']';
     public static final int CODE_CLOSING_CURLY_BRACKET = '}';
     public static final int CODE_CLOSING_ANGLE_BRACKET = '>';
+    public static final int CODE_INVERTED_QUESTION_MARK = 0xBF; // ¿
+    public static final int CODE_INVERTED_EXCLAMATION_MARK = 0xA1; // ¡
 
     /**
      * Special keys code. Must be negative.
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
similarity index 93%
rename from java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
rename to java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index 301b832..2123638 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -19,14 +19,18 @@
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.Log;
+import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.personalization.ContextualDictionary;
+import com.android.inputmethod.latin.personalization.PersonalizationDataChunk;
 import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
 import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
+import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.DistracterFilter;
 import com.android.inputmethod.latin.utils.ExecutorUtils;
 import com.android.inputmethod.latin.utils.LanguageModelParam;
 import com.android.inputmethod.latin.utils.SuggestionResults;
@@ -37,6 +41,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
@@ -45,8 +50,8 @@
 import java.util.concurrent.TimeUnit;
 
 // TODO: Consolidate dictionaries in native code.
-public class DictionaryFacilitatorForSuggest {
-    public static final String TAG = DictionaryFacilitatorForSuggest.class.getSimpleName();
+public class DictionaryFacilitator {
+    public static final String TAG = DictionaryFacilitator.class.getSimpleName();
 
     // HACK: This threshold is being used when adding a capitalized entry in the User History
     // dictionary.
@@ -57,6 +62,7 @@
     private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0);
     // To synchronize assigning mDictionaries to ensure closing dictionaries.
     private final Object mLock = new Object();
+    private final DistracterFilter mDistracterFilter;
 
     private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTION =
             new String[] {
@@ -162,7 +168,17 @@
         public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable);
     }
 
-    public DictionaryFacilitatorForSuggest() {}
+    public DictionaryFacilitator() {
+        mDistracterFilter = new DistracterFilter.EmptyDistracterFilter();
+    }
+
+    public DictionaryFacilitator(final DistracterFilter distracterFilter) {
+        mDistracterFilter = distracterFilter;
+    }
+
+    public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) {
+        mDistracterFilter.updateEnabledSubtypes(enabledSubtypes);
+    }
 
     public Locale getLocale() {
         return mDictionaries.mLocale;
@@ -321,6 +337,7 @@
         for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTION) {
             dictionaries.closeDict(dictType);
         }
+        mDistracterFilter.close();
     }
 
     // The main dictionary could have been loaded asynchronously.  Don't cache the return value
@@ -537,9 +554,16 @@
         personalizationDict.clear();
     }
 
-    public void addMultipleDictionaryEntriesToPersonalizationDictionary(
-            final ArrayList<LanguageModelParam> languageModelParams,
+    public void addEntriesToPersonalizationDictionary(
+            final PersonalizationDataChunk personalizationDataChunk,
+            final SpacingAndPunctuations spacingAndPunctuations,
             final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) {
+        final ArrayList<LanguageModelParam> languageModelParams =
+                LanguageModelParam.createLanguageModelParamsFrom(
+                        personalizationDataChunk.mTokens,
+                        personalizationDataChunk.mTimestampInSeconds,
+                        this /* dictionaryFacilitator */, spacingAndPunctuations,
+                        mDistracterFilter);
         final ExpandableBinaryDictionary personalizationDict =
                 mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION);
         if (personalizationDict == null || languageModelParams == null
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 217b0b1..34d5f71 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -81,9 +81,10 @@
 import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
 import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.CapsModeUtils;
+import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.CoordinateUtils;
 import com.android.inputmethod.latin.utils.DialogUtils;
-import com.android.inputmethod.latin.utils.DistracterFilter;
+import com.android.inputmethod.latin.utils.DistracterFilterUsingSuggestion;
 import com.android.inputmethod.latin.utils.ImportantNoticeUtils;
 import com.android.inputmethod.latin.utils.IntentUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
@@ -95,6 +96,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.TimeUnit;
 
@@ -103,7 +105,7 @@
  */
 public class LatinIME extends InputMethodService implements KeyboardActionListener,
         SuggestionStripView.Listener, SuggestionStripViewAccessor,
-        DictionaryFacilitatorForSuggest.DictionaryInitializationListener,
+        DictionaryFacilitator.DictionaryInitializationListener,
         ImportantNoticeDialog.ImportantNoticeDialogListener {
     private static final String TAG = LatinIME.class.getSimpleName();
     private static final boolean TRACE = false;
@@ -122,8 +124,10 @@
     private static final String SCHEME_PACKAGE = "package";
 
     private final Settings mSettings;
+    private final DictionaryFacilitator mDictionaryFacilitator =
+            new DictionaryFacilitator(new DistracterFilterUsingSuggestion(this /* context */));
     private final InputLogic mInputLogic = new InputLogic(this /* LatinIME */,
-            this /* SuggestionStripViewAccessor */);
+            this /* SuggestionStripViewAccessor */, mDictionaryFacilitator);
     // We expect to have only one decoder in almost all cases, hence the default capacity of 1.
     // If it turns out we need several, it will get grown seamlessly.
     final SparseArray<HardwareEventDecoder> mHardwareEventDecoders
@@ -493,8 +497,7 @@
 
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
-            ResearchLogger.getInstance().initDictionary(
-                    mInputLogic.mSuggest.mDictionaryFacilitator);
+            ResearchLogger.getInstance().initDictionary(mDictionaryFacilitator);
         }
 
         // Register to receive ringer mode change and network state change.
@@ -538,13 +541,13 @@
         if (!mHandler.hasPendingReopenDictionaries()) {
             resetSuggestForLocale(locale);
         }
+        mDictionaryFacilitator.updateEnabledSubtypes(mRichImm.getMyEnabledInputMethodSubtypeList(
+                true /* allowsImplicitlySelectedSubtypes */));
         refreshPersonalizationDictionarySession();
         StatsUtils.onLoadSettings(currentSettingsValues);
     }
 
     private void refreshPersonalizationDictionarySession() {
-        final DictionaryFacilitatorForSuggest dictionaryFacilitator =
-                mInputLogic.mSuggest.mDictionaryFacilitator;
         final boolean shouldKeepUserHistoryDictionaries;
         final boolean shouldKeepPersonalizationDictionaries;
         if (mSettings.getCurrent().mUsePersonalizedDicts) {
@@ -559,16 +562,14 @@
         if (!shouldKeepUserHistoryDictionaries) {
             // Remove user history dictionaries.
             PersonalizationHelper.removeAllUserHistoryDictionaries(this);
-            dictionaryFacilitator.clearUserHistoryDictionary();
+            mDictionaryFacilitator.clearUserHistoryDictionary();
         }
         if (!shouldKeepPersonalizationDictionaries) {
             // Remove personalization dictionaries.
             PersonalizationHelper.removeAllPersonalizationDictionaries(this);
             PersonalizationDictionarySessionRegistrar.resetAll(this);
         } else {
-            final DistracterFilter distracterFilter = createDistracterFilter();
-            PersonalizationDictionarySessionRegistrar.init(
-                    this, dictionaryFacilitator, distracterFilter);
+            PersonalizationDictionarySessionRegistrar.init(this, mDictionaryFacilitator);
         }
     }
 
@@ -606,10 +607,8 @@
      * @param locale the locale
      */
     private void resetSuggestForLocale(final Locale locale) {
-        final DictionaryFacilitatorForSuggest dictionaryFacilitator =
-                mInputLogic.mSuggest.mDictionaryFacilitator;
         final SettingsValues settingsValues = mSettings.getCurrent();
-        dictionaryFacilitator.resetDictionaries(this /* context */, locale,
+        mDictionaryFacilitator.resetDictionaries(this /* context */, locale,
                 settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts,
                 false /* forceReloadMainDictionary */, this);
         if (settingsValues.mCorrectionEnabled) {
@@ -622,17 +621,15 @@
      * Reset suggest by loading the main dictionary of the current locale.
      */
     /* package private */ void resetSuggestMainDict() {
-        final DictionaryFacilitatorForSuggest dictionaryFacilitator =
-                mInputLogic.mSuggest.mDictionaryFacilitator;
         final SettingsValues settingsValues = mSettings.getCurrent();
-        dictionaryFacilitator.resetDictionaries(this /* context */,
-                dictionaryFacilitator.getLocale(), settingsValues.mUseContactsDict,
+        mDictionaryFacilitator.resetDictionaries(this /* context */,
+                mDictionaryFacilitator.getLocale(), settingsValues.mUseContactsDict,
                 settingsValues.mUsePersonalizedDicts, true /* forceReloadMainDictionary */, this);
     }
 
     @Override
     public void onDestroy() {
-        mInputLogic.mSuggest.mDictionaryFacilitator.closeDictionaries();
+        mDictionaryFacilitator.closeDictionaries();
         mSettings.onDestroy();
         unregisterReceiver(mConnectivityAndRingerModeChangeReceiver);
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
@@ -666,9 +663,8 @@
             mInputLogic.mConnection.finishComposingText();
             mInputLogic.mConnection.endBatchEdit();
         }
-        final DistracterFilter distracterFilter = createDistracterFilter();
         PersonalizationDictionarySessionRegistrar.onConfigurationChanged(this, conf,
-                mInputLogic.mSuggest.mDictionaryFacilitator, distracterFilter);
+                mDictionaryFacilitator);
         super.onConfigurationChanged(conf);
     }
 
@@ -841,7 +837,8 @@
             currentSettingsValues = mSettings.getCurrent();
 
             if (currentSettingsValues.mCorrectionEnabled) {
-                suggest.setAutoCorrectionThreshold(currentSettingsValues.mAutoCorrectionThreshold);
+                suggest.setAutoCorrectionThreshold(
+                        currentSettingsValues.mAutoCorrectionThreshold);
             }
 
             switcher.loadKeyboard(editorInfo, currentSettingsValues, getCurrentAutoCapsState(),
@@ -870,7 +867,7 @@
         mHandler.cancelUpdateSuggestionStrip();
 
         mainKeyboardView.setMainDictionaryAvailability(
-                suggest.mDictionaryFacilitator.hasInitializedMainDictionary());
+                mDictionaryFacilitator.hasInitializedMainDictionary());
         mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
                 currentSettingsValues.mKeyPreviewPopupDismissDelay);
         mainKeyboardView.setSlidingKeyInputPreviewEnabled(
@@ -1167,8 +1164,7 @@
         } else {
             wordToEdit = word;
         }
-        mInputLogic.mSuggest.mDictionaryFacilitator.addWordToUserDictionary(
-                this /* context */, wordToEdit);
+        mDictionaryFacilitator.addWordToUserDictionary(this /* context */, wordToEdit);
     }
 
     // Callback for the {@link SuggestionStripView}, to call when the important notice strip is
@@ -1725,15 +1721,14 @@
     @UsedForTesting
     /* package for test */ void waitForLoadingDictionaries(final long timeout, final TimeUnit unit)
             throws InterruptedException {
-        mInputLogic.mSuggest.mDictionaryFacilitator.waitForLoadingDictionariesForTesting(
-                timeout, unit);
+        mDictionaryFacilitator.waitForLoadingDictionariesForTesting(timeout, unit);
     }
 
     // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
     @UsedForTesting
     /* package for test */ void replaceDictionariesForTest(final Locale locale) {
         final SettingsValues settingsValues = mSettings.getCurrent();
-        mInputLogic.mSuggest.mDictionaryFacilitator.resetDictionaries(this, locale,
+        mDictionaryFacilitator.resetDictionaries(this, locale,
             settingsValues.mUseContactsDict, settingsValues.mUsePersonalizedDicts,
             false /* forceReloadMainDictionary */, this /* listener */);
     }
@@ -1741,25 +1736,21 @@
     // DO NOT USE THIS for any other purpose than testing.
     @UsedForTesting
     /* package for test */ void clearPersonalizedDictionariesForTest() {
-        mInputLogic.mSuggest.mDictionaryFacilitator.clearUserHistoryDictionary();
-        mInputLogic.mSuggest.mDictionaryFacilitator.clearPersonalizationDictionary();
+        mDictionaryFacilitator.clearUserHistoryDictionary();
+        mDictionaryFacilitator.clearPersonalizationDictionary();
     }
 
     @UsedForTesting
-    /* package for test */ DistracterFilter createDistracterFilter() {
-        // Return an empty distracter filter when this method is called before onCreate().
-        return (mRichImm != null) ? new DistracterFilter(this /* Context */,
-                mRichImm.getMyEnabledInputMethodSubtypeList(
-                        true /* allowsImplicitlySelectedSubtypes */)) : new DistracterFilter();
+    /* package for test */ List<InputMethodSubtype> getEnabledSubtypesForTest() {
+        return (mRichImm != null) ? mRichImm.getMyEnabledInputMethodSubtypeList(
+                true /* allowsImplicitlySelectedSubtypes */) : new ArrayList<InputMethodSubtype>();
     }
 
     public void dumpDictionaryForDebug(final String dictName) {
-        final DictionaryFacilitatorForSuggest dictionaryFacilitator =
-                mInputLogic.mSuggest.mDictionaryFacilitator;
-        if (dictionaryFacilitator.getLocale() == null) {
+        if (mDictionaryFacilitator.getLocale() == null) {
             resetSuggest();
         }
-        mInputLogic.mSuggest.mDictionaryFacilitator.dumpDictionaryForDebug(dictName);
+        mDictionaryFacilitator.dumpDictionaryForDebug(dictName);
     }
 
     public void debugDumpStateAndCrashWithException(final String context) {
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 43daee4..1eccf2c 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -52,11 +52,14 @@
     private static final int SUPPRESS_SUGGEST_THRESHOLD = -2000000000;
 
     private static final boolean DBG = LatinImeLogger.sDBG;
-    public final DictionaryFacilitatorForSuggest mDictionaryFacilitator =
-            new DictionaryFacilitatorForSuggest();
+    private final DictionaryFacilitator mDictionaryFacilitator;
 
     private float mAutoCorrectionThreshold;
 
+    public Suggest(final DictionaryFacilitator dictionaryFacilitator) {
+        mDictionaryFacilitator = dictionaryFacilitator;
+    }
+
     public Locale getLocale() {
         return mDictionaryFacilitator.getLocale();
     }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 7536ff9..78d4bc8 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -32,7 +32,7 @@
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest;
+import com.android.inputmethod.latin.DictionaryFacilitator;
 import com.android.inputmethod.latin.InputPointers;
 import com.android.inputmethod.latin.LastComposedWord;
 import com.android.inputmethod.latin.LatinIME;
@@ -79,7 +79,8 @@
     private int mSpaceState;
     // Never null
     public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
-    public final Suggest mSuggest = new Suggest();
+    public final Suggest mSuggest;
+    private final DictionaryFacilitator mDictionaryFacilitator;
 
     public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
     public final WordComposer mWordComposer;
@@ -102,14 +103,19 @@
      * Create a new instance of the input logic.
      * @param latinIME the instance of the parent LatinIME. We should remove this when we can.
      * @param suggestionStripViewAccessor an object to access the suggestion strip view.
+     * @param dictionaryFacilitator facilitator for getting suggestions and updating user history
+     * dictionary.
      */
     public InputLogic(final LatinIME latinIME,
-            final SuggestionStripViewAccessor suggestionStripViewAccessor) {
+            final SuggestionStripViewAccessor suggestionStripViewAccessor,
+            final DictionaryFacilitator dictionaryFacilitator) {
         mLatinIME = latinIME;
         mSuggestionStripViewAccessor = suggestionStripViewAccessor;
         mWordComposer = new WordComposer();
         mConnection = new RichInputConnection(latinIME);
         mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
+        mSuggest = new Suggest(dictionaryFacilitator);
+        mDictionaryFacilitator = dictionaryFacilitator;
     }
 
     /**
@@ -173,7 +179,7 @@
         final InputLogicHandler inputLogicHandler = mInputLogicHandler;
         mInputLogicHandler = InputLogicHandler.NULL_HANDLER;
         inputLogicHandler.destroy();
-        mSuggest.mDictionaryFacilitator.closeDictionaries();
+        mDictionaryFacilitator.closeDictionaries();
     }
 
     /**
@@ -295,18 +301,16 @@
 
         // We should show the "Touch again to save" hint if the user pressed the first entry
         // AND it's in none of our current dictionaries (main, user or otherwise).
-        final DictionaryFacilitatorForSuggest dictionaryFacilitator =
-                mSuggest.mDictionaryFacilitator;
         final boolean showingAddToDictionaryHint =
                 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
                         || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
-                        && !dictionaryFacilitator.isValidWord(suggestion, true /* ignoreCase */);
+                        && !mDictionaryFacilitator.isValidWord(suggestion, true /* ignoreCase */);
 
         if (settingsValues.mIsInternal) {
             LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         }
-        if (showingAddToDictionaryHint && dictionaryFacilitator.isUserDictionaryEnabled()) {
+        if (showingAddToDictionaryHint && mDictionaryFacilitator.isUserDictionaryEnabled()) {
             mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion);
         } else {
             // If we're not showing the "Touch again to save", then update the suggestion strip.
@@ -1250,7 +1254,7 @@
                 mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps();
         final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds(
                 System.currentTimeMillis());
-        mSuggest.mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized,
+        mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized,
                 prevWordsInfo, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive);
     }
 
@@ -1462,8 +1466,7 @@
         }
         mConnection.deleteSurroundingText(deleteLength, 0);
         if (!TextUtils.isEmpty(prevWordsInfo.mPrevWord) && !TextUtils.isEmpty(committedWord)) {
-            mSuggest.mDictionaryFacilitator.cancelAddingUserHistory(
-                    prevWordsInfo, committedWordString);
+            mDictionaryFacilitator.cancelAddingUserHistory(prevWordsInfo, committedWordString);
         }
         final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
         final SpannableString textToCommit = new SpannableString(stringToCommit);
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java
new file mode 100644
index 0000000..9d72de8
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDataChunk.java
@@ -0,0 +1,37 @@
+/*
+ * 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.personalization;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+public class PersonalizationDataChunk {
+    public final boolean mInputByUser;
+    public final List<String> mTokens;
+    public final int mTimestampInSeconds;
+    public final String mPackageName;
+    public final Locale mlocale = null;
+
+    public PersonalizationDataChunk(boolean inputByUser, final List<String> tokens,
+            final int timestampInSeconds, final String packageName) {
+        mInputByUser = inputByUser;
+        mTokens = Collections.unmodifiableList(tokens);
+        mTimestampInSeconds = timestampInSeconds;
+        mPackageName = packageName;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java
index 9bef7a1..4506440 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationDictionarySessionRegistrar.java
@@ -19,18 +19,15 @@
 import android.content.Context;
 import android.content.res.Configuration;
 
-import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest;
-import com.android.inputmethod.latin.utils.DistracterFilter;
+import com.android.inputmethod.latin.DictionaryFacilitator;
 
 public class PersonalizationDictionarySessionRegistrar {
     public static void init(final Context context,
-            final DictionaryFacilitatorForSuggest dictionaryFacilitator,
-            final DistracterFilter distracterFilter) {
+            final DictionaryFacilitator dictionaryFacilitator) {
     }
 
     public static void onConfigurationChanged(final Context context, final Configuration conf,
-            final DictionaryFacilitatorForSuggest dictionaryFacilitator,
-            final DistracterFilter distracterFilter) {
+            final DictionaryFacilitator dictionaryFacilitator) {
     }
 
     public static void onUpdateData(final Context context, final String type) {
diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
index 702688f..9362193 100644
--- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
@@ -62,6 +62,22 @@
     }
 
     /**
+     * Helper method to find out if a code point is starting punctuation.
+     *
+     * This include the Unicode START_PUNCTUATION category, but also some other symbols that are
+     * starting, like the inverted question mark or the double quote.
+     *
+     * @param codePoint the code point
+     * @return true if it's starting punctuation, false otherwise.
+     */
+    private static boolean isStartPunctuation(final int codePoint) {
+        return (codePoint == Constants.CODE_DOUBLE_QUOTE || codePoint == Constants.CODE_SINGLE_QUOTE
+                || codePoint == Constants.CODE_INVERTED_QUESTION_MARK
+                || codePoint == Constants.CODE_INVERTED_EXCLAMATION_MARK
+                || Character.getType(codePoint) == Character.START_PUNCTUATION);
+    }
+
+    /**
      * Determine what caps mode should be in effect at the current offset in
      * the text. Only the mode bits set in <var>reqModes</var> will be
      * checked. Note that the caps mode flags here are explicitly defined
@@ -115,8 +131,7 @@
         } else {
             for (i = cs.length(); i > 0; i--) {
                 final char c = cs.charAt(i - 1);
-                if (c != Constants.CODE_DOUBLE_QUOTE && c != Constants.CODE_SINGLE_QUOTE
-                        && Character.getType(c) != Character.START_PUNCTUATION) {
+                if (!isStartPunctuation(c)) {
                     break;
                 }
             }
@@ -210,11 +225,14 @@
 
         // We found out that we have a period. We need to determine if this is a full stop or
         // otherwise sentence-ending period, or an abbreviation like "e.g.". An abbreviation
-        // looks like (\w\.){2,}
+        // looks like (\w\.){2,}. Moreover, in German, you put periods after digits for dates
+        // and some other things, and in German specifically we need to not go into autocaps after
+        // a whitespace-digits-period sequence.
         // To find out, we will have a simple state machine with the following states :
-        // START, WORD, PERIOD, ABBREVIATION
+        // START, WORD, PERIOD, ABBREVIATION, NUMBER
         // On START : (just before the first period)
         //           letter => WORD
+        //           digit => NUMBER if German; end with caps otherwise
         //           whitespace => end with no caps (it was a stand-alone period)
         //           otherwise => end with caps (several periods/symbols in a row)
         // On WORD : (within the word just before the first period)
@@ -228,6 +246,11 @@
         //           letter => LETTER
         //           period => PERIOD
         //           otherwise => end with no caps (it was an abbreviation)
+        // On NUMBER : (period immediately preceded by one or more digits)
+        //           digit => NUMBER
+        //           letter => LETTER (promote to word)
+        //           otherwise => end with no caps (it was a whitespace-digits-period sequence,
+        //            or a punctuation-digits-period sequence like "11.11.")
         // "Not an abbreviation" in the above chart essentially covers cases like "...yes.". This
         // should capitalize.
 
@@ -235,6 +258,7 @@
         final int WORD = 1;
         final int PERIOD = 2;
         final int LETTER = 3;
+        final int NUMBER = 4;
         final int caps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
                 | TextUtils.CAP_MODE_SENTENCES) & reqModes;
         final int noCaps = (TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS) & reqModes;
@@ -247,6 +271,8 @@
                     state = WORD;
                 } else if (Character.isWhitespace(c)) {
                     return noCaps;
+                } else if (Character.isDigit(c) && spacingAndPunctuations.mUsesGermanRules) {
+                    state = NUMBER;
                 } else {
                     return caps;
                 }
@@ -275,6 +301,15 @@
                 } else {
                     return noCaps;
                 }
+                break;
+            case NUMBER:
+                if (Character.isLetter(c)) {
+                    state = WORD;
+                } else if (Character.isDigit(c)) {
+                    state = NUMBER;
+                } else {
+                    return noCaps;
+                }
             }
         }
         // Here we arrived at the start of the line. This should behave exactly like whitespace.
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
index f1057da..6e0fab3 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilter.java
@@ -16,129 +16,14 @@
 
 package com.android.inputmethod.latin.utils;
 
-import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
-import android.content.Context;
-import android.content.res.Resources;
-import android.text.InputType;
-import android.util.Log;
-import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.keyboard.KeyboardLayoutSet;
-import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.PrevWordsInfo;
-import com.android.inputmethod.latin.Suggest;
-import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
-import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.WordComposer;
 
-/**
- * This class is used to prevent distracters being added to personalization
- * or user history dictionaries
- */
-public class DistracterFilter {
-    private static final String TAG = DistracterFilter.class.getSimpleName();
-
-    private static final long TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS = 120;
-
-    private final Context mContext;
-    private final Map<Locale, InputMethodSubtype> mLocaleToSubtypeMap;
-    private final Map<Locale, Keyboard> mLocaleToKeyboardMap;
-    private final Suggest mSuggest;
-    private Keyboard mKeyboard;
-
-    // If the score of the top suggestion exceeds this value, the tested word (e.g.,
-    // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distracter to
-    // words in dictionary. The greater the threshold is, the less likely the tested word would
-    // become a distracter, which means the tested word will be more likely to be added to
-    // the dictionary.
-    private static final float DISTRACTER_WORD_SCORE_THRESHOLD = 2.0f;
-
-    // Create empty distracter filter.
-    public DistracterFilter() {
-        this(null, new ArrayList<InputMethodSubtype>());
-    }
-
-    /**
-     * Create a DistracterFilter instance.
-     *
-     * @param context the context.
-     * @param enabledSubtypes the enabled subtypes.
-     */
-    public DistracterFilter(final Context context, final List<InputMethodSubtype> enabledSubtypes) {
-        mContext = context;
-        mLocaleToSubtypeMap = new HashMap<>();
-        if (enabledSubtypes != null) {
-            for (final InputMethodSubtype subtype : enabledSubtypes) {
-                final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype);
-                if (mLocaleToSubtypeMap.containsKey(locale)) {
-                    // Multiple subtypes are enabled for one locale.
-                    // TODO: Investigate what we should do for this case.
-                    continue;
-                }
-                mLocaleToSubtypeMap.put(locale, subtype);
-            }
-        }
-        mLocaleToKeyboardMap = new HashMap<>();
-        mSuggest = new Suggest();
-        mKeyboard = null;
-    }
-
-    private static boolean suggestionExceedsDistracterThreshold(
-            final SuggestedWordInfo suggestion, final String consideredWord,
-            final float distracterThreshold) {
-        if (null != suggestion) {
-            final int suggestionScore = suggestion.mScore;
-            final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
-                    consideredWord, suggestion.mWord, suggestionScore);
-            if (normalizedScore > distracterThreshold) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private void loadKeyboardForLocale(final Locale newLocale) {
-        final Keyboard cachedKeyboard = mLocaleToKeyboardMap.get(newLocale);
-        if (cachedKeyboard != null) {
-            mKeyboard = cachedKeyboard;
-            return;
-        }
-        final InputMethodSubtype subtype = mLocaleToSubtypeMap.get(newLocale);
-        if (subtype == null) {
-            return;
-        }
-        final EditorInfo editorInfo = new EditorInfo();
-        editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
-        final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
-                mContext, editorInfo);
-        final Resources res = mContext.getResources();
-        final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
-        final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res);
-        builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
-        builder.setSubtype(subtype);
-        builder.setIsSpellChecker(false /* isSpellChecker */);
-        final KeyboardLayoutSet layoutSet = builder.build();
-        mKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
-    }
-
-    private void loadDictionariesForLocale(final Locale newlocale) throws InterruptedException {
-        mSuggest.mDictionaryFacilitator.resetDictionaries(mContext, newlocale,
-                false /* useContactsDict */, false /* usePersonalizedDicts */,
-                false /* forceReloadMainDictionary */, null /* listener */);
-        mSuggest.mDictionaryFacilitator.waitForLoadingMainDictionary(
-                TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS, TimeUnit.SECONDS);
-    }
-
+public interface DistracterFilter {
     /**
      * Determine whether a word is a distracter to words in dictionaries.
      *
@@ -149,56 +34,25 @@
      * @return true if testedWord is a distracter, otherwise false.
      */
     public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo,
-            final String testedWord, final Locale locale) {
-        if (locale == null) {
+            final String testedWord, final Locale locale);
+
+    public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes);
+
+    public void close();
+
+    public static final class EmptyDistracterFilter implements DistracterFilter {
+        @Override
+        public boolean isDistracterToWordsInDictionaries(PrevWordsInfo prevWordsInfo,
+                String testedWord, Locale locale) {
             return false;
         }
-        if (!locale.equals(mSuggest.mDictionaryFacilitator.getLocale())) {
-            if (!mLocaleToSubtypeMap.containsKey(locale)) {
-                Log.e(TAG, "Locale " + locale + " is not enabled.");
-                // TODO: Investigate what we should do for disabled locales.
-                return false;
-            }
-            loadKeyboardForLocale(locale);
-            // Reset dictionaries for the locale.
-            try {
-                loadDictionariesForLocale(locale);
-            } catch (final InterruptedException e) {
-                Log.e(TAG, "Interrupted while waiting for loading dicts in DistracterFilter", e);
-                return false;
-            }
-        }
-        if (mKeyboard == null) {
-            return false;
-        }
-        final WordComposer composer = new WordComposer();
-        final int[] codePoints = StringUtils.toCodePointArray(testedWord);
-        final int[] coordinates = mKeyboard.getCoordinates(codePoints);
-        composer.setComposingWord(codePoints, coordinates, prevWordsInfo);
 
-        final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(testedWord);
-        final String consideredWord = trailingSingleQuotesCount > 0 ?
-                testedWord.substring(0, testedWord.length() - trailingSingleQuotesCount) :
-                testedWord;
-        final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
-        final OnGetSuggestedWordsCallback callback = new OnGetSuggestedWordsCallback() {
-            @Override
-            public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
-                if (suggestedWords != null && suggestedWords.size() > 1) {
-                    // The suggestedWordInfo at 0 is the typed word. The 1st suggestion from
-                    // the decoder is at index 1.
-                    final SuggestedWordInfo firstSuggestion = suggestedWords.getInfo(1);
-                    final boolean hasStrongDistractor = suggestionExceedsDistracterThreshold(
-                            firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD);
-                    holder.set(hasStrongDistractor);
-                }
-            }
-        };
-        mSuggest.getSuggestedWords(composer, prevWordsInfo, mKeyboard.getProximityInfo(),
-                true /* blockOffensiveWords */, true /* isCorrectionEnbaled */,
-                null /* additionalFeaturesOptions */, 0 /* sessionId */,
-                SuggestedWords.NOT_A_SEQUENCE_NUMBER, callback);
+        @Override
+        public void close() {
+        }
 
-        return holder.get(false /* defaultValue */, Constants.GET_SUGGESTED_WORDS_TIMEOUT);
+        @Override
+        public void updateEnabledSubtypes(List<InputMethodSubtype> enabledSubtypes) {
+        }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterUsingSuggestion.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterUsingSuggestion.java
new file mode 100644
index 0000000..92033b7
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterUsingSuggestion.java
@@ -0,0 +1,227 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.InputType;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.DictionaryFacilitator;
+import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.Suggest;
+import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.WordComposer;
+
+/**
+ * This class is used to prevent distracters being added to personalization
+ * or user history dictionaries
+ */
+public class DistracterFilterUsingSuggestion implements DistracterFilter {
+    private static final String TAG = DistracterFilterUsingSuggestion.class.getSimpleName();
+
+    private static final long TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS = 120;
+
+    private final Context mContext;
+    private final Map<Locale, InputMethodSubtype> mLocaleToSubtypeMap;
+    private final Map<Locale, Keyboard> mLocaleToKeyboardMap;
+    private final DictionaryFacilitator mDictionaryFacilitator;
+    private final Suggest mSuggest;
+    private Keyboard mKeyboard;
+    private final Object mLock = new Object();
+
+    // If the score of the top suggestion exceeds this value, the tested word (e.g.,
+    // an OOV, a misspelling, or an in-vocabulary word) would be considered as a distracter to
+    // words in dictionary. The greater the threshold is, the less likely the tested word would
+    // become a distracter, which means the tested word will be more likely to be added to
+    // the dictionary.
+    private static final float DISTRACTER_WORD_SCORE_THRESHOLD = 2.0f;
+
+    /**
+     * Create a DistracterFilter instance.
+     *
+     * @param context the context.
+     */
+    public DistracterFilterUsingSuggestion(final Context context) {
+        mContext = context;
+        mLocaleToSubtypeMap = new HashMap<>();
+        mLocaleToKeyboardMap = new HashMap<>();
+        mDictionaryFacilitator = new DictionaryFacilitator();
+        mSuggest = new Suggest(mDictionaryFacilitator);
+        mKeyboard = null;
+    }
+
+    @Override
+    public void close() {
+        mDictionaryFacilitator.closeDictionaries();
+    }
+
+    @Override
+    public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) {
+        final Map<Locale, InputMethodSubtype> newLocaleToSubtypeMap = new HashMap<>();
+        if (enabledSubtypes != null) {
+            for (final InputMethodSubtype subtype : enabledSubtypes) {
+                final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype);
+                if (newLocaleToSubtypeMap.containsKey(locale)) {
+                    // Multiple subtypes are enabled for one locale.
+                    // TODO: Investigate what we should do for this case.
+                    continue;
+                }
+                newLocaleToSubtypeMap.put(locale, subtype);
+            }
+        }
+        if (mLocaleToSubtypeMap.equals(newLocaleToSubtypeMap)) {
+            // Enabled subtypes have not been changed.
+            return;
+        }
+        synchronized (mLock) {
+            mLocaleToSubtypeMap.clear();
+            mLocaleToSubtypeMap.putAll(newLocaleToSubtypeMap);
+            mLocaleToKeyboardMap.clear();
+        }
+    }
+
+    private static boolean suggestionExceedsDistracterThreshold(
+            final SuggestedWordInfo suggestion, final String consideredWord,
+            final float distracterThreshold) {
+        if (null != suggestion) {
+            final int suggestionScore = suggestion.mScore;
+            final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
+                    consideredWord, suggestion.mWord, suggestionScore);
+            if (normalizedScore > distracterThreshold) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void loadKeyboardForLocale(final Locale newLocale) {
+        final Keyboard cachedKeyboard = mLocaleToKeyboardMap.get(newLocale);
+        if (cachedKeyboard != null) {
+            mKeyboard = cachedKeyboard;
+            return;
+        }
+        final InputMethodSubtype subtype;
+        synchronized (mLock) {
+            subtype = mLocaleToSubtypeMap.get(newLocale);
+        }
+        if (subtype == null) {
+            return;
+        }
+        final EditorInfo editorInfo = new EditorInfo();
+        editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
+        final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
+                mContext, editorInfo);
+        final Resources res = mContext.getResources();
+        final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
+        final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res);
+        builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
+        builder.setSubtype(subtype);
+        builder.setIsSpellChecker(false /* isSpellChecker */);
+        final KeyboardLayoutSet layoutSet = builder.build();
+        mKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
+    }
+
+    private void loadDictionariesForLocale(final Locale newlocale) throws InterruptedException {
+        mDictionaryFacilitator.resetDictionaries(mContext, newlocale,
+                false /* useContactsDict */, false /* usePersonalizedDicts */,
+                false /* forceReloadMainDictionary */, null /* listener */);
+        mDictionaryFacilitator.waitForLoadingMainDictionary(
+                TIMEOUT_TO_WAIT_LOADING_DICTIONARIES_IN_SECONDS, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Determine whether a word is a distracter to words in dictionaries.
+     *
+     * @param prevWordsInfo the information of previous words.
+     * @param testedWord the word that will be tested to see whether it is a distracter to words
+     *                   in dictionaries.
+     * @param locale the locale of word.
+     * @return true if testedWord is a distracter, otherwise false.
+     */
+    @Override
+    public boolean isDistracterToWordsInDictionaries(final PrevWordsInfo prevWordsInfo,
+            final String testedWord, final Locale locale) {
+        if (locale == null) {
+            return false;
+        }
+        if (!locale.equals(mDictionaryFacilitator.getLocale())) {
+            synchronized (mLock) {
+                if (!mLocaleToSubtypeMap.containsKey(locale)) {
+                    Log.e(TAG, "Locale " + locale + " is not enabled.");
+                    // TODO: Investigate what we should do for disabled locales.
+                    return false;
+                }
+                loadKeyboardForLocale(locale);
+                // Reset dictionaries for the locale.
+                try {
+                    loadDictionariesForLocale(locale);
+                } catch (final InterruptedException e) {
+                    Log.e(TAG, "Interrupted while waiting for loading dicts in DistracterFilter",
+                            e);
+                    return false;
+                }
+            }
+        }
+        if (mKeyboard == null) {
+            return false;
+        }
+        final WordComposer composer = new WordComposer();
+        final int[] codePoints = StringUtils.toCodePointArray(testedWord);
+        final int[] coordinates = mKeyboard.getCoordinates(codePoints);
+        composer.setComposingWord(codePoints, coordinates, prevWordsInfo);
+
+        final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(testedWord);
+        final String consideredWord = trailingSingleQuotesCount > 0 ?
+                testedWord.substring(0, testedWord.length() - trailingSingleQuotesCount) :
+                testedWord;
+        final AsyncResultHolder<Boolean> holder = new AsyncResultHolder<Boolean>();
+        final OnGetSuggestedWordsCallback callback = new OnGetSuggestedWordsCallback() {
+            @Override
+            public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
+                if (suggestedWords != null && suggestedWords.size() > 1) {
+                    // The suggestedWordInfo at 0 is the typed word. The 1st suggestion from
+                    // the decoder is at index 1.
+                    final SuggestedWordInfo firstSuggestion = suggestedWords.getInfo(1);
+                    final boolean hasStrongDistractor = suggestionExceedsDistracterThreshold(
+                            firstSuggestion, consideredWord, DISTRACTER_WORD_SCORE_THRESHOLD);
+                    holder.set(hasStrongDistractor);
+                }
+            }
+        };
+        mSuggest.getSuggestedWords(composer, prevWordsInfo, mKeyboard.getProximityInfo(),
+                true /* blockOffensiveWords */, true /* isCorrectionEnbaled */,
+                null /* additionalFeaturesOptions */, 0 /* sessionId */,
+                SuggestedWords.NOT_A_SEQUENCE_NUMBER, callback);
+
+        return holder.get(false /* defaultValue */, Constants.GET_SUGGESTED_WORDS_TIMEOUT);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
index aaf4a40..430efdd 100644
--- a/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
+++ b/java/src/com/android/inputmethod/latin/utils/LanguageModelParam.java
@@ -19,11 +19,12 @@
 import android.util.Log;
 
 import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest;
+import com.android.inputmethod.latin.DictionaryFacilitator;
 import com.android.inputmethod.latin.PrevWordsInfo;
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 
 import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 
 // Note: this class is used as a parameter type of a native method. You should be careful when you
@@ -79,8 +80,8 @@
 
     // Process a list of words and return a list of {@link LanguageModelParam} objects.
     public static ArrayList<LanguageModelParam> createLanguageModelParamsFrom(
-            final ArrayList<String> tokens, final int timestamp,
-            final DictionaryFacilitatorForSuggest dictionaryFacilitator,
+            final List<String> tokens, final int timestamp,
+            final DictionaryFacilitator dictionaryFacilitator,
             final SpacingAndPunctuations spacingAndPunctuations,
             final DistracterFilter distracterFilter) {
         final ArrayList<LanguageModelParam> languageModelParams =
@@ -124,7 +125,7 @@
 
     private static LanguageModelParam detectWhetherVaildWordOrNotAndGetLanguageModelParam(
             final PrevWordsInfo prevWordsInfo, final String targetWord, final int timestamp,
-            final DictionaryFacilitatorForSuggest dictionaryFacilitator,
+            final DictionaryFacilitator dictionaryFacilitator,
             final DistracterFilter distracterFilter) {
         final Locale locale = dictionaryFacilitator.getLocale();
         if (locale == null) {
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index ffdb43c..3806ac7 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -20,7 +20,7 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest;
+import com.android.inputmethod.latin.DictionaryFacilitator;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
 import java.io.IOException;
@@ -75,7 +75,7 @@
     // The size of the n-grams logged.  E.g. N_GRAM_SIZE = 2 means to sample bigrams.
     public static final int N_GRAM_SIZE = 2;
 
-    private final DictionaryFacilitatorForSuggest mDictionaryFacilitator;
+    private final DictionaryFacilitator mDictionaryFacilitator;
     @UsedForTesting
     private Dictionary mDictionaryForTesting;
     private boolean mIsStopping = false;
@@ -87,7 +87,7 @@
     /* package for test */ int mNumWordsUntilSafeToSample;
 
     public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore,
-            final DictionaryFacilitatorForSuggest dictionaryFacilitator) {
+            final DictionaryFacilitator dictionaryFacilitator) {
         super(N_GRAM_SIZE + wordsBetweenSamples);
         mNumWordsBetweenNGrams = wordsBetweenSamples;
         mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore;
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index d907dd1..d73f9c4 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -52,7 +52,7 @@
 import com.android.inputmethod.keyboard.KeyboardView;
 import com.android.inputmethod.keyboard.MainKeyboardView;
 import com.android.inputmethod.latin.Constants;
-import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest;
+import com.android.inputmethod.latin.DictionaryFacilitator;
 import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputConnection;
@@ -167,7 +167,7 @@
     protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
 
     // used to check whether words are not unique
-    private DictionaryFacilitatorForSuggest mDictionaryFacilitator;
+    private DictionaryFacilitator mDictionaryFacilitator;
     private MainKeyboardView mMainKeyboardView;
     // TODO: Check whether a superclass can be used instead of LatinIME.
     /* package for test */ LatinIME mLatinIME;
@@ -656,7 +656,7 @@
         mInFeedbackDialog = false;
     }
 
-    public void initDictionary(final DictionaryFacilitatorForSuggest dictionaryFacilitator) {
+    public void initDictionary(final DictionaryFacilitator dictionaryFacilitator) {
         mDictionaryFacilitator = dictionaryFacilitator;
         // MainLogBuffer now has an out-of-date Suggest object.  Close down MainLogBuffer and create
         // a new one.
diff --git a/tests/src/com/android/inputmethod/latin/DistracterFilterTest.java b/tests/src/com/android/inputmethod/latin/DistracterFilterTest.java
index e98f9ea..ddbc8ac 100644
--- a/tests/src/com/android/inputmethod/latin/DistracterFilterTest.java
+++ b/tests/src/com/android/inputmethod/latin/DistracterFilterTest.java
@@ -20,19 +20,20 @@
 
 import android.test.suitebuilder.annotation.LargeTest;
 
-import com.android.inputmethod.latin.utils.DistracterFilter;
+import com.android.inputmethod.latin.utils.DistracterFilterUsingSuggestion;
 
 /**
  * Unit test for DistracterFilter
  */
 @LargeTest
 public class DistracterFilterTest extends InputTestsBase {
-    private DistracterFilter mDistracterFilter;
+    private DistracterFilterUsingSuggestion mDistracterFilter;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        mDistracterFilter = mLatinIME.createDistracterFilter();
+        mDistracterFilter = new DistracterFilterUsingSuggestion(getContext());
+        mDistracterFilter.updateEnabledSubtypes(mLatinIME.getEnabledSubtypesForTest());
     }
 
     public void testIsDistractorToWordsInDictionaries() {
diff --git a/tests/src/com/android/inputmethod/latin/ShiftModeTests.java b/tests/src/com/android/inputmethod/latin/ShiftModeTests.java
index 6fc9df7..de5538e 100644
--- a/tests/src/com/android/inputmethod/latin/ShiftModeTests.java
+++ b/tests/src/com/android/inputmethod/latin/ShiftModeTests.java
@@ -78,4 +78,35 @@
         runMessages();
         assertTrue("Caps after a while after repeating Backspace a lot", isCapsModeAutoShifted());
     }
+
+    public void testAutoCapsAfterDigitsPeriod() {
+        changeLanguage("en");
+        type("On 22.11.");
+        assertFalse("(English) Auto caps after digits-period", isCapsModeAutoShifted());
+        type(" ");
+        assertTrue("(English) Auto caps after digits-period-whitespace", isCapsModeAutoShifted());
+        mEditText.setText("");
+        changeLanguage("fr");
+        type("Le 22.");
+        assertFalse("(French) Auto caps after digits-period", isCapsModeAutoShifted());
+        type(" ");
+        assertTrue("(French) Auto caps after digits-period-whitespace", isCapsModeAutoShifted());
+        mEditText.setText("");
+        changeLanguage("de");
+        type("Am 22.");
+        assertFalse("(German) Auto caps after digits-period", isCapsModeAutoShifted());
+        type(" ");
+        // For German, no auto-caps in this case
+        assertFalse("(German) Auto caps after digits-period-whitespace", isCapsModeAutoShifted());
+    }
+
+    public void testAutoCapsAfterInvertedMarks() {
+        changeLanguage("es");
+        assertTrue("(Spanish) Auto caps at start", isCapsModeAutoShifted());
+        type("Hey. ¿");
+        assertTrue("(Spanish) Auto caps after inverted what", isCapsModeAutoShifted());
+        mEditText.setText("");
+        type("¡");
+        assertTrue("(Spanish) Auto caps after inverted bang", isCapsModeAutoShifted());
+    }
 }