Extract dict operations from Suggest to a new class.

Bug: 8187060
Change-Id: I77775aa50763158d99753c2312fa11fe14267aef
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
new file mode 100644
index 0000000..b464891
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorForSuggest.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2013 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 android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.latin.Suggest.SuggestInitializationListener;
+import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.personalization.PersonalizationDictionary;
+import com.android.inputmethod.latin.personalization.PersonalizationHelper;
+import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
+import com.android.inputmethod.latin.settings.SettingsValues;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+// TODO: Consolidate dictionaries in native code.
+public class DictionaryFacilitatorForSuggest {
+    public static final String TAG = DictionaryFacilitatorForSuggest.class.getSimpleName();
+
+    private final Context mContext;
+    private final Locale mLocale;
+
+    private final ConcurrentHashMap<String, Dictionary> mDictionaries =
+            CollectionUtils.newConcurrentHashMap();
+    private HashSet<String> mOnlyDictionarySetForDebug = null;
+
+    private Dictionary mMainDictionary;
+    private ContactsBinaryDictionary mContactsDictionary;
+    private UserBinaryDictionary mUserDictionary;
+    private UserHistoryDictionary mUserHistoryDictionary;
+    private PersonalizationDictionary mPersonalizationDictionary;
+
+    @UsedForTesting
+    private boolean mIsCurrentlyWaitingForMainDictionary = false;
+
+    public DictionaryFacilitatorForSuggest(final Context context, final Locale locale,
+            final SettingsValues settingsValues, final SuggestInitializationListener listener) {
+        resetMainDict(context, locale, listener);
+        mContext = context;
+        mLocale = locale;
+        // initialize a debug flag for the personalization
+        if (settingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
+            mOnlyDictionarySetForDebug = new HashSet<String>();
+            mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION);
+        }
+        setUserDictionary(new UserBinaryDictionary(context, locale));
+    }
+
+    @UsedForTesting
+    DictionaryFacilitatorForSuggest(final Context context, final AssetFileAddress[] dictionaryList,
+            final Locale locale) {
+        final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList,
+                false /* useFullEditDistance */, locale);
+        mContext = context;
+        mLocale = locale;
+        mMainDictionary = mainDict;
+        addOrReplaceDictionary(Dictionary.TYPE_MAIN, mainDict);
+    }
+
+    public void close() {
+        final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
+        dictionaries.addAll(mDictionaries.values());
+        for (final Dictionary dictionary : dictionaries) {
+            dictionary.close();
+        }
+        mMainDictionary = null;
+        mContactsDictionary = null;
+        mUserDictionary = null;
+        mUserHistoryDictionary = null;
+        mPersonalizationDictionary = null;
+    }
+
+    public void resetMainDict(final Context context, final Locale locale,
+            final SuggestInitializationListener listener) {
+        mIsCurrentlyWaitingForMainDictionary = true;
+        mMainDictionary = null;
+        if (listener != null) {
+            listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+        }
+        new Thread("InitializeBinaryDictionary") {
+            @Override
+            public void run() {
+                final DictionaryCollection newMainDict =
+                        DictionaryFactory.createMainDictionaryFromManager(context, locale);
+                setMainDictionary(newMainDict);
+                if (listener != null) {
+                    listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
+                }
+                mIsCurrentlyWaitingForMainDictionary = false;
+            }
+        }.start();
+    }
+
+    // The main dictionary could have been loaded asynchronously.  Don't cache the return value
+    // of this method.
+    public boolean hasMainDictionary() {
+        return null != mMainDictionary && mMainDictionary.isInitialized();
+    }
+
+    @UsedForTesting
+    public boolean isCurrentlyWaitingForMainDictionary() {
+        return mIsCurrentlyWaitingForMainDictionary;
+    }
+
+    public Dictionary getMainDictionary() {
+        return mMainDictionary;
+    }
+
+    private void setMainDictionary(final Dictionary mainDictionary) {
+        mMainDictionary = mainDictionary;
+        addOrReplaceDictionary(Dictionary.TYPE_MAIN, mainDictionary);
+    }
+
+    /**
+     * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
+     * before the main dictionary, if set. This refers to the system-managed user dictionary.
+     */
+    @UsedForTesting
+    public void setUserDictionary(final UserBinaryDictionary userDictionary) {
+        mUserDictionary = userDictionary;
+        addOrReplaceDictionary(Dictionary.TYPE_USER, userDictionary);
+    }
+
+    /**
+     * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove
+     * the contacts dictionary by passing null to this method. In this case no contacts dictionary
+     * won't be used.
+     */
+    @UsedForTesting
+    public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) {
+        mContactsDictionary = contactsDictionary;
+        addOrReplaceDictionary(Dictionary.TYPE_CONTACTS, contactsDictionary);
+    }
+
+    private void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) {
+        mUserHistoryDictionary = userHistoryDictionary;
+        addOrReplaceDictionary(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
+    }
+
+    private void setPersonalizationDictionary(
+            final PersonalizationDictionary personalizationDictionary) {
+        mPersonalizationDictionary = personalizationDictionary;
+        addOrReplaceDictionary(Dictionary.TYPE_PERSONALIZATION, personalizationDictionary);
+    }
+
+    /**
+     * Set dictionaries that can be turned off according to the user settings.
+     *
+     * @param oldDictionaryFacilitator the instance having old dictionaries
+     * @param settingsValues current SettingsValues
+     */
+    public void setAdditionalDictionaries(
+            final DictionaryFacilitatorForSuggest oldDictionaryFacilitator,
+            final SettingsValues settingsValues) {
+        // Contacts dictionary
+        resetContactsDictionary(null != oldDictionaryFacilitator ?
+                oldDictionaryFacilitator.mContactsDictionary : null, settingsValues);
+        // User history dictionary & Personalization dictionary
+        resetPersonalizedDictionaries(oldDictionaryFacilitator, settingsValues);
+    }
+
+    /**
+     * Set the user history dictionary and personalization dictionary according to the user
+     * settings.
+     *
+     * @param oldDictionaryFacilitator the instance that has been used
+     * @param settingsValues current settingsValues
+     */
+    // TODO: Consolidate resetPersonalizedDictionaries() and resetContactsDictionary(). Call up the
+    // new method for each dictionary.
+    private void resetPersonalizedDictionaries(
+            final DictionaryFacilitatorForSuggest oldDictionaryFacilitator,
+            final SettingsValues settingsValues) {
+        final boolean shouldSetDictionaries = settingsValues.mUsePersonalizedDicts;
+
+        final UserHistoryDictionary oldUserHistoryDictionary = (null == oldDictionaryFacilitator) ?
+                null : oldDictionaryFacilitator.mUserHistoryDictionary;
+        final PersonalizationDictionary oldPersonalizationDictionary =
+                (null == oldDictionaryFacilitator) ? null :
+                        oldDictionaryFacilitator.mPersonalizationDictionary;
+        final UserHistoryDictionary userHistoryDictionaryToUse;
+        final PersonalizationDictionary personalizationDictionaryToUse;
+        if (!shouldSetDictionaries) {
+            userHistoryDictionaryToUse = null;
+            personalizationDictionaryToUse = null;
+        } else {
+            if (null != oldUserHistoryDictionary
+                    && oldUserHistoryDictionary.mLocale.equals(mLocale)) {
+                userHistoryDictionaryToUse = oldUserHistoryDictionary;
+            } else {
+                userHistoryDictionaryToUse =
+                        PersonalizationHelper.getUserHistoryDictionary(mContext, mLocale);
+            }
+            if (null != oldPersonalizationDictionary
+                    && oldPersonalizationDictionary.mLocale.equals(mLocale)) {
+                personalizationDictionaryToUse = oldPersonalizationDictionary;
+            } else {
+                personalizationDictionaryToUse =
+                        PersonalizationHelper.getPersonalizationDictionary(mContext, mLocale);
+            }
+        }
+        setUserHistoryDictionary(userHistoryDictionaryToUse);
+        setPersonalizationDictionary(personalizationDictionaryToUse);
+    }
+
+    /**
+     * Set the contacts dictionary according to the user settings.
+     *
+     * This method takes an optional contacts dictionary to use when the locale hasn't changed
+     * since the contacts dictionary can be opened or closed as necessary depending on the settings.
+     *
+     * @param oldContactsDictionary an optional dictionary to use, or null
+     * @param settingsValues current settingsValues
+     */
+    private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary,
+            final SettingsValues settingsValues) {
+        final boolean shouldSetDictionary = settingsValues.mUseContactsDict;
+        final ContactsBinaryDictionary dictionaryToUse;
+        if (!shouldSetDictionary) {
+            // Make sure the dictionary is closed. If it is already closed, this is a no-op,
+            // so it's safe to call it anyways.
+            if (null != oldContactsDictionary) oldContactsDictionary.close();
+            dictionaryToUse = null;
+        } else {
+            if (null != oldContactsDictionary) {
+                if (!oldContactsDictionary.mLocale.equals(mLocale)) {
+                    // If the locale has changed then recreate the contacts dictionary. This
+                    // allows locale dependent rules for handling bigram name predictions.
+                    oldContactsDictionary.close();
+                    dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
+                } else {
+                    // Make sure the old contacts dictionary is opened. If it is already open,
+                    // this is a no-op, so it's safe to call it anyways.
+                    oldContactsDictionary.reopen(mContext);
+                    dictionaryToUse = oldContactsDictionary;
+                }
+            } else {
+                dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
+            }
+        }
+        setContactsDictionary(dictionaryToUse);
+    }
+
+    public boolean isUserDictionaryEnabled() {
+        if (mUserDictionary == null) {
+            return false;
+        }
+        return mUserDictionary.mEnabled;
+    }
+
+    public void addWordToUserDictionary(String word) {
+        if (mUserDictionary == null) {
+            return;
+        }
+        mUserDictionary.addWordToUserDictionary(word);
+    }
+
+    public String addToUserHistory(final WordComposer wordComposer, final String previousWord,
+            final String suggestion) {
+        if (mUserHistoryDictionary == null) {
+            return null;
+        }
+        final String secondWord;
+        if (wordComposer.wasAutoCapitalized() && !wordComposer.isMostlyCaps()) {
+            secondWord = suggestion.toLowerCase(mLocale);
+        } else {
+            secondWord = suggestion;
+        }
+        // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
+        // We don't add words with 0-frequency (assuming they would be profanity etc.).
+        final int maxFreq = getMaxFrequency(suggestion);
+        if (maxFreq == 0) {
+            return null;
+        }
+        final boolean isValid = maxFreq > 0;
+        final int timeStamp = (int)TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis()));
+        mUserHistoryDictionary.addToDictionary(previousWord, secondWord, isValid, timeStamp);
+        return previousWord;
+    }
+
+    public void cancelAddingUserHistory(final String previousWord, final String committedWord) {
+        if (mUserHistoryDictionary != null) {
+            mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
+        }
+    }
+
+    // TODO: Revise the way to fusion suggestion results.
+    public void getSuggestions(final WordComposer composer,
+            final String prevWord, final ProximityInfo proximityInfo,
+            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
+            final int sessionId, final Set<SuggestedWordInfo> suggestionSet) {
+        for (final String key : mDictionaries.keySet()) {
+            final Dictionary dictionary = mDictionaries.get(key);
+            suggestionSet.addAll(dictionary.getSuggestionsWithSessionId(composer, prevWord,
+                    proximityInfo, blockOffensiveWords, additionalFeaturesOptions, sessionId));
+        }
+    }
+
+    public boolean isValidWord(final String word, final boolean ignoreCase) {
+        if (TextUtils.isEmpty(word)) {
+            return false;
+        }
+        final String lowerCasedWord = word.toLowerCase(mLocale);
+        for (final String key : mDictionaries.keySet()) {
+            final Dictionary dictionary = mDictionaries.get(key);
+            // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
+            // managing to get null in here. Presumably the language is changing to a language with
+            // no main dictionary and the monkey manages to type a whole word before the thread
+            // that reads the dictionary is started or something?
+            // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
+            // would be immutable once it's finished initializing, but concretely a null test is
+            // probably good enough for the time being.
+            if (null == dictionary) continue;
+            if (dictionary.isValidWord(word)
+                    || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private int getMaxFrequency(final String word) {
+        if (TextUtils.isEmpty(word)) {
+            return Dictionary.NOT_A_PROBABILITY;
+        }
+        int maxFreq = -1;
+        for (final String key : mDictionaries.keySet()) {
+            final Dictionary dictionary = mDictionaries.get(key);
+            if (null == dictionary) continue;
+            final int tempFreq = dictionary.getFrequency(word);
+            if (tempFreq >= maxFreq) {
+                maxFreq = tempFreq;
+            }
+        }
+        return maxFreq;
+    }
+
+    private void addOrReplaceDictionary(final String key, final Dictionary dict) {
+        if (mOnlyDictionarySetForDebug != null && !mOnlyDictionarySetForDebug.contains(key)) {
+            Log.w(TAG, "Ignore add " + key + " dictionary for debug.");
+            return;
+        }
+        final Dictionary oldDict;
+        if (dict == null) {
+            oldDict = mDictionaries.remove(key);
+        } else {
+            oldDict = mDictionaries.put(key, dict);
+        }
+        if (oldDict != null && dict != oldDict) {
+            oldDict.close();
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 182356c..77def63 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -472,7 +472,7 @@
         initSuggest();
 
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mInputLogic.mSuggest);
+            ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
         }
 
         // Register to receive ringer mode change and network state change.
@@ -516,8 +516,9 @@
         if (!mHandler.hasPendingReopenDictionaries() && mInputLogic.mSuggest != null) {
             // May need to reset dictionaries depending on the user settings.
             // TODO: Quit setting dictionaries from LatinIME.
-            mInputLogic.mSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */,
-                   currentSettingsValues);
+            mInputLogic.mSuggest.mDictionaryFacilitator.setAdditionalDictionaries(
+                    mInputLogic.mSuggest.mDictionaryFacilitator /* oldDictionaryFacilitator */,
+                    currentSettingsValues);
         }
         if (currentSettingsValues.mUsePersonalizedDicts) {
             PersonalizationDictionarySessionRegistrar.init(this);
@@ -561,10 +562,12 @@
         }
 
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.getInstance().initSuggest(newSuggest);
+            ResearchLogger.getInstance().initDictionary(newSuggest.mDictionaryFacilitator);
         }
         // TODO: Quit setting dictionaries from LatinIME.
-        newSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */, settingsValues);
+        newSuggest.mDictionaryFacilitator.setAdditionalDictionaries(
+                (mInputLogic.mSuggest == null) ? null : mInputLogic.mSuggest.mDictionaryFacilitator
+                        /* oldDictionaryFacilitator */, settingsValues);
         final Suggest oldSuggest = mInputLogic.mSuggest;
         mInputLogic.mSuggest = newSuggest;
         if (oldSuggest != null) oldSuggest.close();
@@ -572,7 +575,7 @@
 
     /* package private */ void resetSuggestMainDict() {
         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
-        mInputLogic.mSuggest.resetMainDict(this, subtypeLocale,
+        mInputLogic.mSuggest.mDictionaryFacilitator.resetMainDict(this, subtypeLocale,
                 this /* SuggestInitializationListener */);
     }
 
@@ -822,7 +825,7 @@
         mHandler.cancelDoubleSpacePeriodTimer();
 
         mainKeyboardView.setMainDictionaryAvailability(null != suggest
-                ? suggest.hasMainDictionary() : false);
+                ? suggest.mDictionaryFacilitator.hasMainDictionary() : false);
         mainKeyboardView.setKeyPreviewPopupEnabled(currentSettingsValues.mKeyPreviewPopupOn,
                 currentSettingsValues.mKeyPreviewPopupDismissDelay);
         mainKeyboardView.setSlidingKeyInputPreviewEnabled(
@@ -1203,7 +1206,7 @@
         } else {
             wordToEdit = word;
         }
-        mInputLogic.mSuggest.addWordToUserDictionary(wordToEdit);
+        mInputLogic.mSuggest.mDictionaryFacilitator.addWordToUserDictionary(wordToEdit);
     }
 
     public void displaySettingsDialog() {
@@ -1724,13 +1727,15 @@
                         || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
                         && suggest != null
                         // If the suggestion is not in the dictionary, the hint should be shown.
-                        && !suggest.isValidWord(suggestion, true);
+                        && !suggest.mDictionaryFacilitator.isValidWord(suggestion,
+                                true /* ignoreCase */);
 
         if (currentSettings.mIsInternal) {
             LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE,
                     Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
         }
-        if (showingAddToDictionaryHint && suggest.isUserDictionaryEnabled()) {
+        if (showingAddToDictionaryHint
+                && suggest.mDictionaryFacilitator.isUserDictionaryEnabled()) {
             mSuggestionStripView.showAddToDictionaryHint(
                     suggestion, currentSettings.mHintToSaveText);
         } else {
@@ -1963,13 +1968,13 @@
     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
     @UsedForTesting
     /* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
-        return mInputLogic.mSuggest.isCurrentlyWaitingForMainDictionary();
+        return mInputLogic.mSuggest.mDictionaryFacilitator.isCurrentlyWaitingForMainDictionary();
     }
 
     // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
     @UsedForTesting
     /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
-        mInputLogic.mSuggest.resetMainDict(this, locale, null);
+        mInputLogic.mSuggest.mDictionaryFacilitator.resetMainDict(this, locale, null);
     }
 
     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 1684122..3f38323 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -18,14 +18,10 @@
 
 import android.content.Context;
 import android.text.TextUtils;
-import android.util.Log;
 
 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.PersonalizationDictionary;
-import com.android.inputmethod.latin.personalization.PersonalizationHelper;
-import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
 import com.android.inputmethod.latin.settings.SettingsValues;
 import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
 import com.android.inputmethod.latin.utils.BoundedTreeSet;
@@ -34,16 +30,12 @@
 
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.HashSet;
 import java.util.Locale;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeUnit;
 
 /**
  * This class loads a dictionary and provides a list of suggestions for a given sequence of
  * characters. This includes corrections and completions.
  */
-// TODO: Separate dictionary operations from suggestions handling logic.
 public final class Suggest {
     public static final String TAG = Suggest.class.getSimpleName();
 
@@ -69,276 +61,25 @@
 
     private static final boolean DBG = LatinImeLogger.sDBG;
 
-    private final ConcurrentHashMap<String, Dictionary> mDictionaries =
-            CollectionUtils.newConcurrentHashMap();
-    private HashSet<String> mOnlyDictionarySetForDebug = null;
-    private Dictionary mMainDictionary;
-    private ContactsBinaryDictionary mContactsDictionary;
-    private UserBinaryDictionary mUserDictionary;
-    private UserHistoryDictionary mUserHistoryDictionary;
-    private PersonalizationDictionary mPersonalizationDictionary;
-    @UsedForTesting
-    private boolean mIsCurrentlyWaitingForMainDictionary = false;
+    public final DictionaryFacilitatorForSuggest mDictionaryFacilitator;
 
     private float mAutoCorrectionThreshold;
 
     // Locale used for upper- and title-casing words
     public final Locale mLocale;
 
-    private final Context mContext;
-
     public Suggest(final Context context, final Locale locale, final SettingsValues settingsValues,
             final SuggestInitializationListener listener) {
-        initAsynchronously(context, locale, listener);
         mLocale = locale;
-        mContext = context;
-        // initialize a debug flag for the personalization
-        if (settingsValues.mUseOnlyPersonalizationDictionaryForDebug) {
-            mOnlyDictionarySetForDebug = new HashSet<String>();
-            mOnlyDictionarySetForDebug.add(Dictionary.TYPE_PERSONALIZATION);
-        }
-        setUserDictionary(new UserBinaryDictionary(context, locale));
+        mDictionaryFacilitator = new DictionaryFacilitatorForSuggest(context, locale,
+                settingsValues, listener);
     }
 
     @UsedForTesting
     Suggest(final Context context, final AssetFileAddress[] dictionaryList, final Locale locale) {
-        final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(dictionaryList,
-                false /* useFullEditDistance */, locale);
         mLocale = locale;
-        mContext = context;
-        mMainDictionary = mainDict;
-        addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, mainDict);
-    }
-
-    private void initAsynchronously(final Context context, final Locale locale,
-            final SuggestInitializationListener listener) {
-        resetMainDict(context, locale, listener);
-    }
-
-    private void addOrReplaceDictionaryInternal(final String key, final Dictionary dict) {
-        if (mOnlyDictionarySetForDebug != null && !mOnlyDictionarySetForDebug.contains(key)) {
-            Log.w(TAG, "Ignore add " + key + " dictionary for debug.");
-            return;
-        }
-        addOrReplaceDictionary(mDictionaries, key, dict);
-    }
-
-    private static void addOrReplaceDictionary(
-            final ConcurrentHashMap<String, Dictionary> dictionaries,
-            final String key, final Dictionary dict) {
-        final Dictionary oldDict = (dict == null)
-                ? dictionaries.remove(key)
-                : dictionaries.put(key, dict);
-        if (oldDict != null && dict != oldDict) {
-            oldDict.close();
-        }
-    }
-
-    public void resetMainDict(final Context context, final Locale locale,
-            final SuggestInitializationListener listener) {
-        mIsCurrentlyWaitingForMainDictionary = true;
-        mMainDictionary = null;
-        if (listener != null) {
-            listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
-        }
-        new Thread("InitializeBinaryDictionary") {
-            @Override
-            public void run() {
-                final DictionaryCollection newMainDict =
-                        DictionaryFactory.createMainDictionaryFromManager(context, locale);
-                addOrReplaceDictionaryInternal(Dictionary.TYPE_MAIN, newMainDict);
-                mMainDictionary = newMainDict;
-                if (listener != null) {
-                    listener.onUpdateMainDictionaryAvailability(hasMainDictionary());
-                }
-                mIsCurrentlyWaitingForMainDictionary = false;
-            }
-        }.start();
-    }
-
-    // The main dictionary could have been loaded asynchronously.  Don't cache the return value
-    // of this method.
-    public boolean hasMainDictionary() {
-        return null != mMainDictionary && mMainDictionary.isInitialized();
-    }
-
-    @UsedForTesting
-    public boolean isCurrentlyWaitingForMainDictionary() {
-        return mIsCurrentlyWaitingForMainDictionary;
-    }
-
-    public Dictionary getMainDictionary() {
-        return mMainDictionary;
-    }
-
-    /**
-     * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
-     * before the main dictionary, if set. This refers to the system-managed user dictionary.
-     */
-    @UsedForTesting
-    public void setUserDictionary(final UserBinaryDictionary userDictionary) {
-        mUserDictionary = userDictionary;
-        addOrReplaceDictionaryInternal(Dictionary.TYPE_USER, userDictionary);
-    }
-
-    /**
-     * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove
-     * the contacts dictionary by passing null to this method. In this case no contacts dictionary
-     * won't be used.
-     */
-    @UsedForTesting
-    public void setContactsDictionary(final ContactsBinaryDictionary contactsDictionary) {
-        mContactsDictionary = contactsDictionary;
-        addOrReplaceDictionaryInternal(Dictionary.TYPE_CONTACTS, contactsDictionary);
-    }
-
-    private void setUserHistoryDictionary(final UserHistoryDictionary userHistoryDictionary) {
-        mUserHistoryDictionary = userHistoryDictionary;
-        addOrReplaceDictionaryInternal(Dictionary.TYPE_USER_HISTORY, userHistoryDictionary);
-    }
-
-    private void setPersonalizationDictionary(
-            final PersonalizationDictionary personalizationDictionary) {
-        mPersonalizationDictionary = personalizationDictionary;
-        addOrReplaceDictionaryInternal(Dictionary.TYPE_PERSONALIZATION, personalizationDictionary);
-    }
-
-    /**
-     * Set dictionaries that can be turned off according to the user settings.
-     *
-     * @param oldSuggest the instance having old dictionaries
-     * @param settingsValues current SettingsValues
-     */
-    public void setAdditionalDictionaries(final Suggest oldSuggest,
-            final SettingsValues settingsValues) {
-        // Contacts dictionary
-        resetContactsDictionary(null != oldSuggest ? oldSuggest.mContactsDictionary : null,
-                settingsValues);
-        // User history dictionary & Personalization dictionary
-        resetPersonalizedDictionaries(oldSuggest, settingsValues);
-    }
-
-    /**
-     * Set the user history dictionary and personalization dictionary according to the user
-     * settings.
-     *
-     * @param oldSuggest the instance that has been used
-     * @param settingsValues current settingsValues
-     */
-    // TODO: Consolidate resetPersonalizedDictionaries() and resetContactsDictionary(). Call up the
-    // new method for each dictionary.
-    private void resetPersonalizedDictionaries(final Suggest oldSuggest,
-            final SettingsValues settingsValues) {
-        final boolean shouldSetDictionaries = settingsValues.mUsePersonalizedDicts;
-
-        final UserHistoryDictionary oldUserHistoryDictionary = (null == oldSuggest) ? null :
-                oldSuggest.mUserHistoryDictionary;
-        final PersonalizationDictionary oldPersonalizationDictionary = (null == oldSuggest) ? null :
-                oldSuggest.mPersonalizationDictionary;
-        final UserHistoryDictionary userHistoryDictionaryToUse;
-        final PersonalizationDictionary personalizationDictionaryToUse;
-        if (!shouldSetDictionaries) {
-            userHistoryDictionaryToUse = null;
-            personalizationDictionaryToUse = null;
-        } else {
-            if (null != oldUserHistoryDictionary
-                    && oldUserHistoryDictionary.mLocale.equals(mLocale)) {
-                userHistoryDictionaryToUse = oldUserHistoryDictionary;
-            } else {
-                userHistoryDictionaryToUse =
-                        PersonalizationHelper.getUserHistoryDictionary(mContext, mLocale);
-            }
-            if (null != oldPersonalizationDictionary
-                    && oldPersonalizationDictionary.mLocale.equals(mLocale)) {
-                personalizationDictionaryToUse = oldPersonalizationDictionary;
-            } else {
-                personalizationDictionaryToUse =
-                        PersonalizationHelper.getPersonalizationDictionary(mContext, mLocale);
-            }
-        }
-        setUserHistoryDictionary(userHistoryDictionaryToUse);
-        setPersonalizationDictionary(personalizationDictionaryToUse);
-    }
-
-    /**
-     * Set the contacts dictionary according to the user settings.
-     *
-     * This method takes an optional contacts dictionary to use when the locale hasn't changed
-     * since the contacts dictionary can be opened or closed as necessary depending on the settings.
-     *
-     * @param oldContactsDictionary an optional dictionary to use, or null
-     * @param settingsValues current settingsValues
-     */
-    private void resetContactsDictionary(final ContactsBinaryDictionary oldContactsDictionary,
-            final SettingsValues settingsValues) {
-        final boolean shouldSetDictionary = settingsValues.mUseContactsDict;
-        final ContactsBinaryDictionary dictionaryToUse;
-        if (!shouldSetDictionary) {
-            // Make sure the dictionary is closed. If it is already closed, this is a no-op,
-            // so it's safe to call it anyways.
-            if (null != oldContactsDictionary) oldContactsDictionary.close();
-            dictionaryToUse = null;
-        } else {
-            if (null != oldContactsDictionary) {
-                if (!oldContactsDictionary.mLocale.equals(mLocale)) {
-                    // If the locale has changed then recreate the contacts dictionary. This
-                    // allows locale dependent rules for handling bigram name predictions.
-                    oldContactsDictionary.close();
-                    dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
-                } else {
-                    // Make sure the old contacts dictionary is opened. If it is already open,
-                    // this is a no-op, so it's safe to call it anyways.
-                    oldContactsDictionary.reopen(mContext);
-                    dictionaryToUse = oldContactsDictionary;
-                }
-            } else {
-                dictionaryToUse = new ContactsBinaryDictionary(mContext, mLocale);
-            }
-        }
-        setContactsDictionary(dictionaryToUse);
-    }
-
-    public boolean isUserDictionaryEnabled() {
-        if (mUserDictionary == null) {
-            return false;
-        }
-        return mUserDictionary.mEnabled;
-    }
-
-    public void addWordToUserDictionary(String word) {
-        if (mUserDictionary == null) {
-            return;
-        }
-        mUserDictionary.addWordToUserDictionary(word);
-    }
-
-    public String addToUserHistory(final WordComposer wordComposer, final String previousWord,
-            final String suggestion) {
-        if (mUserHistoryDictionary == null) {
-            return null;
-        }
-        final String secondWord;
-        if (wordComposer.wasAutoCapitalized() && !wordComposer.isMostlyCaps()) {
-            secondWord = suggestion.toLowerCase(mLocale);
-        } else {
-            secondWord = suggestion;
-        }
-        // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
-        // We don't add words with 0-frequency (assuming they would be profanity etc.).
-        final int maxFreq = getMaxFrequency(suggestion);
-        if (maxFreq == 0) {
-            return null;
-        }
-        final boolean isValid = maxFreq > 0;
-        final int timeStamp = (int)TimeUnit.MILLISECONDS.toSeconds((System.currentTimeMillis()));
-        mUserHistoryDictionary.addToDictionary(previousWord, secondWord, isValid, timeStamp);
-        return previousWord;
-    }
-
-    public void cancelAddingUserHistory(final String previousWord, final String committedWord) {
-        if (mUserHistoryDictionary != null) {
-            mUserHistoryDictionary.cancelAddingUserHistory(previousWord, committedWord);
-        }
+        mDictionaryFacilitator = new DictionaryFacilitatorForSuggest(context, dictionaryList,
+                locale);
     }
 
     public void setAutoCorrectionThreshold(float threshold) {
@@ -392,14 +133,8 @@
         } else {
             wordComposerForLookup = wordComposer;
         }
-
-        for (final String key : mDictionaries.keySet()) {
-            final Dictionary dictionary = mDictionaries.get(key);
-            suggestionsSet.addAll(dictionary.getSuggestions(wordComposerForLookup,
-                    prevWordForBigram, proximityInfo, blockOffensiveWords,
-                    additionalFeaturesOptions));
-        }
-
+        mDictionaryFacilitator.getSuggestions(wordComposer, prevWordForBigram, proximityInfo,
+                blockOffensiveWords, additionalFeaturesOptions, SESSION_TYPING, suggestionsSet);
         final String whitelistedWord;
         if (suggestionsSet.isEmpty()) {
             whitelistedWord = null;
@@ -413,8 +148,8 @@
         // or if it's a 2+ characters non-word (i.e. it's not in the dictionary).
         final boolean allowsToBeAutoCorrected = (null != whitelistedWord
                 && !whitelistedWord.equals(consideredWord))
-                || (consideredWord.length() > 1
-                        && !isValidWord(consideredWord, wordComposer.isFirstCharCapitalized()));
+                || (consideredWord.length() > 1 && !mDictionaryFacilitator.isValidWord(
+                        consideredWord, wordComposer.isFirstCharCapitalized()));
 
         final boolean hasAutoCorrection;
         // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because
@@ -424,7 +159,8 @@
         // the word *would* have been auto-corrected.
         if (!isCorrectionEnabled || !allowsToBeAutoCorrected || !wordComposer.isComposingWord()
                 || suggestionsSet.isEmpty() || wordComposer.hasDigits()
-                || wordComposer.isMostlyCaps() || wordComposer.isResumed() || !hasMainDictionary()
+                || wordComposer.isMostlyCaps() || wordComposer.isResumed()
+                || !mDictionaryFacilitator.hasMainDictionary()
                 || SuggestedWordInfo.KIND_SHORTCUT == suggestionsSet.first().mKind) {
             // If we don't have a main dictionary, we never want to auto-correct. The reason for
             // this is, the user may have a contact whose name happens to match a valid word in
@@ -497,15 +233,8 @@
             final OnGetSuggestedWordsCallback callback) {
         final BoundedTreeSet suggestionsSet = new BoundedTreeSet(sSuggestedWordInfoComparator,
                 MAX_SUGGESTIONS);
-
-        // At second character typed, search the unigrams (scores being affected by bigrams)
-        for (final String key : mDictionaries.keySet()) {
-            final Dictionary dictionary = mDictionaries.get(key);
-            suggestionsSet.addAll(dictionary.getSuggestionsWithSessionId(wordComposer,
-                    prevWordForBigram, proximityInfo, blockOffensiveWords,
-                    additionalFeaturesOptions, sessionId));
-        }
-
+        mDictionaryFacilitator.getSuggestions(wordComposer, prevWordForBigram, proximityInfo,
+                blockOffensiveWords, additionalFeaturesOptions, sessionId, suggestionsSet);
         for (SuggestedWordInfo wordInfo : suggestionsSet) {
             LatinImeLogger.onAddSuggestedWord(wordInfo.mWord, wordInfo.mSourceDict.mDictType);
         }
@@ -618,51 +347,7 @@
                 wordInfo.mAutoCommitFirstWordConfidence);
     }
 
-    public boolean isValidWord(final String word, final boolean ignoreCase) {
-        if (TextUtils.isEmpty(word)) {
-            return false;
-        }
-        final String lowerCasedWord = word.toLowerCase(mLocale);
-        for (final String key : mDictionaries.keySet()) {
-            final Dictionary dictionary = mDictionaries.get(key);
-            // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
-            // managing to get null in here. Presumably the language is changing to a language with
-            // no main dictionary and the monkey manages to type a whole word before the thread
-            // that reads the dictionary is started or something?
-            // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
-            // would be immutable once it's finished initializing, but concretely a null test is
-            // probably good enough for the time being.
-            if (null == dictionary) continue;
-            if (dictionary.isValidWord(word)
-                    || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private int getMaxFrequency(final String word) {
-        if (TextUtils.isEmpty(word)) {
-            return Dictionary.NOT_A_PROBABILITY;
-        }
-        int maxFreq = -1;
-        for (final String key : mDictionaries.keySet()) {
-            final Dictionary dictionary = mDictionaries.get(key);
-            if (null == dictionary) continue;
-            final int tempFreq = dictionary.getFrequency(word);
-            if (tempFreq >= maxFreq) {
-                maxFreq = tempFreq;
-            }
-        }
-        return maxFreq;
-    }
-
     public void close() {
-        final HashSet<Dictionary> dictionaries = CollectionUtils.newHashSet();
-        dictionaries.addAll(mDictionaries.values());
-        for (final Dictionary dictionary : dictionaries) {
-            dictionary.close();
-        }
-        mMainDictionary = null;
+        mDictionaryFacilitator.close();
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 4a4abd7..b417a3e 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -986,7 +986,7 @@
         if (suggest == null) return null;
 
         final String prevWord = mConnection.getNthPreviousWord(settingsValues, 2);
-        return suggest.addToUserHistory(mWordComposer, prevWord, suggestion);
+        return suggest.mDictionaryFacilitator.addToUserHistory(mWordComposer, prevWord, suggestion);
     }
 
     public void performUpdateSuggestionStripSync(final SettingsValues settingsValues,
@@ -1190,7 +1190,8 @@
         mConnection.deleteSurroundingText(deleteLength, 0);
         if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
             if (mSuggest != null) {
-                mSuggest.cancelAddingUserHistory(previousWord, committedWord);
+                mSuggest.mDictionaryFacilitator.cancelAddingUserHistory(
+                        previousWord, committedWord);
             }
         }
         final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 6df7c17..a75663f 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.Suggest;
+import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
 import java.io.IOException;
@@ -75,9 +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;
 
-    // TODO: Remove dependence on Suggest, and pass in Dictionary as a parameter to an appropriate
-    // method.
-    private final Suggest mSuggest;
+    private final DictionaryFacilitatorForSuggest mDictionaryFacilitator;
     @UsedForTesting
     private Dictionary mDictionaryForTesting;
     private boolean mIsStopping = false;
@@ -89,11 +87,11 @@
     /* package for test */ int mNumWordsUntilSafeToSample;
 
     public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore,
-            final Suggest suggest) {
+            final DictionaryFacilitatorForSuggest dictionaryFacilitator) {
         super(N_GRAM_SIZE + wordsBetweenSamples);
         mNumWordsBetweenNGrams = wordsBetweenSamples;
         mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore;
-        mSuggest = suggest;
+        mDictionaryFacilitator = dictionaryFacilitator;
     }
 
     @UsedForTesting
@@ -105,8 +103,10 @@
         if (mDictionaryForTesting != null) {
             return mDictionaryForTesting;
         }
-        if (mSuggest == null || !mSuggest.hasMainDictionary()) return null;
-        return mSuggest.getMainDictionary();
+        if (mDictionaryFacilitator == null || !mDictionaryFacilitator.hasMainDictionary()) {
+            return null;
+        }
+        return mDictionaryFacilitator.getMainDictionary();
     }
 
     public void setIsStopping() {
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 28a647b..4447b1b 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -53,10 +53,10 @@
 import com.android.inputmethod.keyboard.MainKeyboardView;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest;
 import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputConnection;
-import com.android.inputmethod.latin.Suggest;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.utils.InputTypeUtils;
@@ -166,7 +166,7 @@
     protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
 
     // used to check whether words are not unique
-    private Suggest mSuggest;
+    private DictionaryFacilitatorForSuggest mDictionaryFacilitator;
     private MainKeyboardView mMainKeyboardView;
     // TODO: Check whether a superclass can be used instead of LatinIME.
     /* package for test */ LatinIME mLatinIME;
@@ -205,8 +205,7 @@
         return sInstance;
     }
 
-    public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher,
-            final Suggest suggest) {
+    public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) {
         assert latinIME != null;
         mLatinIME = latinIME;
         mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME);
@@ -242,7 +241,7 @@
                 System.currentTimeMillis(), System.nanoTime()), mLatinIME);
         final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
         mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
-                mSuggest) {
+                mDictionaryFacilitator) {
             @Override
             protected void publish(final ArrayList<LogUnit> logUnits,
                     boolean canIncludePrivateData) {
@@ -656,8 +655,8 @@
         mInFeedbackDialog = false;
     }
 
-    public void initSuggest(final Suggest suggest) {
-        mSuggest = suggest;
+    public void initDictionary(final DictionaryFacilitatorForSuggest dictionaryFacilitator) {
+        mDictionaryFacilitator = dictionaryFacilitator;
         // MainLogBuffer now has an out-of-date Suggest object.  Close down MainLogBuffer and create
         // a new one.
         if (mMainLogBuffer != null) {
@@ -666,10 +665,10 @@
     }
 
     private Dictionary getDictionary() {
-        if (mSuggest == null) {
+        if (mDictionaryFacilitator == null) {
             return null;
         }
-        return mSuggest.getMainDictionary();
+        return mDictionaryFacilitator.getMainDictionary();
     }
 
     private void setIsPasswordView(boolean isPasswordView) {