Home | History | Annotate | Download | only in latin
      1 /*
      2 7 * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.Manifest;
     20 import android.content.Context;
     21 import android.text.TextUtils;
     22 import android.util.Log;
     23 import android.util.LruCache;
     24 
     25 import com.android.inputmethod.annotations.UsedForTesting;
     26 import com.android.inputmethod.keyboard.Keyboard;
     27 import com.android.inputmethod.latin.NgramContext.WordInfo;
     28 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
     29 import com.android.inputmethod.latin.common.ComposedData;
     30 import com.android.inputmethod.latin.common.Constants;
     31 import com.android.inputmethod.latin.common.StringUtils;
     32 import com.android.inputmethod.latin.permissions.PermissionsUtil;
     33 import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
     34 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
     35 import com.android.inputmethod.latin.utils.ExecutorUtils;
     36 import com.android.inputmethod.latin.utils.SuggestionResults;
     37 
     38 import java.io.File;
     39 import java.lang.reflect.InvocationTargetException;
     40 import java.lang.reflect.Method;
     41 import java.util.ArrayList;
     42 import java.util.Collections;
     43 import java.util.HashMap;
     44 import java.util.HashSet;
     45 import java.util.List;
     46 import java.util.Locale;
     47 import java.util.Map;
     48 import java.util.concurrent.ConcurrentHashMap;
     49 import java.util.concurrent.CountDownLatch;
     50 import java.util.concurrent.TimeUnit;
     51 
     52 import javax.annotation.Nonnull;
     53 import javax.annotation.Nullable;
     54 
     55 /**
     56  * Facilitates interaction with different kinds of dictionaries. Provides APIs
     57  * to instantiate and select the correct dictionaries (based on language or account),
     58  * update entries and fetch suggestions.
     59  *
     60  * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as
     61  * a client for interacting with dictionaries.
     62  */
     63 public class DictionaryFacilitatorImpl implements DictionaryFacilitator {
     64     // TODO: Consolidate dictionaries in native code.
     65     public static final String TAG = DictionaryFacilitatorImpl.class.getSimpleName();
     66 
     67     // HACK: This threshold is being used when adding a capitalized entry in the User History
     68     // dictionary.
     69     private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140;
     70 
     71     private DictionaryGroup mDictionaryGroup = new DictionaryGroup();
     72     private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0);
     73     // To synchronize assigning mDictionaryGroup to ensure closing dictionaries.
     74     private final Object mLock = new Object();
     75 
     76     public static final Map<String, Class<? extends ExpandableBinaryDictionary>>
     77             DICT_TYPE_TO_CLASS = new HashMap<>();
     78 
     79     static {
     80         DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class);
     81         DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class);
     82         DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class);
     83     }
     84 
     85     private static final String DICT_FACTORY_METHOD_NAME = "getDictionary";
     86     private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES =
     87             new Class[] { Context.class, Locale.class, File.class, String.class, String.class };
     88 
     89     private LruCache<String, Boolean> mValidSpellingWordReadCache;
     90     private LruCache<String, Boolean> mValidSpellingWordWriteCache;
     91 
     92     @Override
     93     public void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache) {
     94         mValidSpellingWordReadCache = cache;
     95     }
     96 
     97     @Override
     98     public void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache) {
     99         mValidSpellingWordWriteCache = cache;
    100     }
    101 
    102     @Override
    103     public boolean isForLocale(final Locale locale) {
    104         return locale != null && locale.equals(mDictionaryGroup.mLocale);
    105     }
    106 
    107     /**
    108      * Returns whether this facilitator is exactly for this account.
    109      *
    110      * @param account the account to test against.
    111      */
    112     public boolean isForAccount(@Nullable final String account) {
    113         return TextUtils.equals(mDictionaryGroup.mAccount, account);
    114     }
    115 
    116     /**
    117      * A group of dictionaries that work together for a single language.
    118      */
    119     private static class DictionaryGroup {
    120         // TODO: Add null analysis annotations.
    121         // TODO: Run evaluation to determine a reasonable value for these constants. The current
    122         // values are ad-hoc and chosen without any particular care or methodology.
    123         public static final float WEIGHT_FOR_MOST_PROBABLE_LANGUAGE = 1.0f;
    124         public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f;
    125         public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f;
    126 
    127         /**
    128          * The locale associated with the dictionary group.
    129          */
    130         @Nullable public final Locale mLocale;
    131 
    132         /**
    133          * The user account associated with the dictionary group.
    134          */
    135         @Nullable public final String mAccount;
    136 
    137         @Nullable private Dictionary mMainDict;
    138         // Confidence that the most probable language is actually the language the user is
    139         // typing in. For now, this is simply the number of times a word from this language
    140         // has been committed in a row.
    141         private int mConfidence = 0;
    142 
    143         public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
    144         public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE;
    145         public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap =
    146                 new ConcurrentHashMap<>();
    147 
    148         public DictionaryGroup() {
    149             this(null /* locale */, null /* mainDict */, null /* account */,
    150                     Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */);
    151         }
    152 
    153         public DictionaryGroup(@Nullable final Locale locale,
    154                 @Nullable final Dictionary mainDict,
    155                 @Nullable final String account,
    156                 final Map<String, ExpandableBinaryDictionary> subDicts) {
    157             mLocale = locale;
    158             mAccount = account;
    159             // The main dictionary can be asynchronously loaded.
    160             setMainDict(mainDict);
    161             for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) {
    162                 setSubDict(entry.getKey(), entry.getValue());
    163             }
    164         }
    165 
    166         private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) {
    167             if (dict != null) {
    168                 mSubDictMap.put(dictType, dict);
    169             }
    170         }
    171 
    172         public void setMainDict(final Dictionary mainDict) {
    173             // Close old dictionary if exists. Main dictionary can be assigned multiple times.
    174             final Dictionary oldDict = mMainDict;
    175             mMainDict = mainDict;
    176             if (oldDict != null && mainDict != oldDict) {
    177                 oldDict.close();
    178             }
    179         }
    180 
    181         public Dictionary getDict(final String dictType) {
    182             if (Dictionary.TYPE_MAIN.equals(dictType)) {
    183                 return mMainDict;
    184             }
    185             return getSubDict(dictType);
    186         }
    187 
    188         public ExpandableBinaryDictionary getSubDict(final String dictType) {
    189             return mSubDictMap.get(dictType);
    190         }
    191 
    192         public boolean hasDict(final String dictType, @Nullable final String account) {
    193             if (Dictionary.TYPE_MAIN.equals(dictType)) {
    194                 return mMainDict != null;
    195             }
    196             if (Dictionary.TYPE_USER_HISTORY.equals(dictType) &&
    197                     !TextUtils.equals(account, mAccount)) {
    198                 // If the dictionary type is user history, & if the account doesn't match,
    199                 // return immediately. If the account matches, continue looking it up in the
    200                 // sub dictionary map.
    201                 return false;
    202             }
    203             return mSubDictMap.containsKey(dictType);
    204         }
    205 
    206         public void closeDict(final String dictType) {
    207             final Dictionary dict;
    208             if (Dictionary.TYPE_MAIN.equals(dictType)) {
    209                 dict = mMainDict;
    210             } else {
    211                 dict = mSubDictMap.remove(dictType);
    212             }
    213             if (dict != null) {
    214                 dict.close();
    215             }
    216         }
    217     }
    218 
    219     public DictionaryFacilitatorImpl() {
    220     }
    221 
    222     @Override
    223     public void onStartInput() {
    224     }
    225 
    226     @Override
    227     public void onFinishInput(Context context) {
    228     }
    229 
    230     @Override
    231     public boolean isActive() {
    232         return mDictionaryGroup.mLocale != null;
    233     }
    234 
    235     @Override
    236     public Locale getLocale() {
    237         return mDictionaryGroup.mLocale;
    238     }
    239 
    240     @Override
    241     public boolean usesContacts() {
    242         return mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTACTS) != null;
    243     }
    244 
    245     @Override
    246     public String getAccount() {
    247         return null;
    248     }
    249 
    250     @Nullable
    251     private static ExpandableBinaryDictionary getSubDict(final String dictType,
    252             final Context context, final Locale locale, final File dictFile,
    253             final String dictNamePrefix, @Nullable final String account) {
    254         final Class<? extends ExpandableBinaryDictionary> dictClass =
    255                 DICT_TYPE_TO_CLASS.get(dictType);
    256         if (dictClass == null) {
    257             return null;
    258         }
    259         try {
    260             final Method factoryMethod = dictClass.getMethod(DICT_FACTORY_METHOD_NAME,
    261                     DICT_FACTORY_METHOD_ARG_TYPES);
    262             final Object dict = factoryMethod.invoke(null /* obj */,
    263                     new Object[] { context, locale, dictFile, dictNamePrefix, account });
    264             return (ExpandableBinaryDictionary) dict;
    265         } catch (final NoSuchMethodException | SecurityException | IllegalAccessException
    266                 | IllegalArgumentException | InvocationTargetException e) {
    267             Log.e(TAG, "Cannot create dictionary: " + dictType, e);
    268             return null;
    269         }
    270     }
    271 
    272     @Nullable
    273     static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup,
    274             final Locale locale) {
    275         return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null;
    276     }
    277 
    278     @Override
    279     public void resetDictionaries(
    280             final Context context,
    281             final Locale newLocale,
    282             final boolean useContactsDict,
    283             final boolean usePersonalizedDicts,
    284             final boolean forceReloadMainDictionary,
    285             @Nullable final String account,
    286             final String dictNamePrefix,
    287             @Nullable final DictionaryInitializationListener listener) {
    288         final HashMap<Locale, ArrayList<String>> existingDictionariesToCleanup = new HashMap<>();
    289         // TODO: Make subDictTypesToUse configurable by resource or a static final list.
    290         final HashSet<String> subDictTypesToUse = new HashSet<>();
    291         subDictTypesToUse.add(Dictionary.TYPE_USER);
    292 
    293         // Do not use contacts dictionary if we do not have permissions to read contacts.
    294         final boolean contactsPermissionGranted = PermissionsUtil.checkAllPermissionsGranted(
    295                 context, Manifest.permission.READ_CONTACTS);
    296         if (useContactsDict && contactsPermissionGranted) {
    297             subDictTypesToUse.add(Dictionary.TYPE_CONTACTS);
    298         }
    299         if (usePersonalizedDicts) {
    300             subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY);
    301         }
    302 
    303         // Gather all dictionaries. We'll remove them from the list to clean up later.
    304         final ArrayList<String> dictTypeForLocale = new ArrayList<>();
    305         existingDictionariesToCleanup.put(newLocale, dictTypeForLocale);
    306         final DictionaryGroup currentDictionaryGroupForLocale =
    307                 findDictionaryGroupWithLocale(mDictionaryGroup, newLocale);
    308         if (currentDictionaryGroupForLocale != null) {
    309             for (final String dictType : DYNAMIC_DICTIONARY_TYPES) {
    310                 if (currentDictionaryGroupForLocale.hasDict(dictType, account)) {
    311                     dictTypeForLocale.add(dictType);
    312                 }
    313             }
    314             if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) {
    315                 dictTypeForLocale.add(Dictionary.TYPE_MAIN);
    316             }
    317         }
    318 
    319         final DictionaryGroup dictionaryGroupForLocale =
    320                 findDictionaryGroupWithLocale(mDictionaryGroup, newLocale);
    321         final ArrayList<String> dictTypesToCleanupForLocale =
    322                 existingDictionariesToCleanup.get(newLocale);
    323         final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale);
    324 
    325         final Dictionary mainDict;
    326         if (forceReloadMainDictionary || noExistingDictsForThisLocale
    327                 || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) {
    328             mainDict = null;
    329         } else {
    330             mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN);
    331             dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN);
    332         }
    333 
    334         final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
    335         for (final String subDictType : subDictTypesToUse) {
    336             final ExpandableBinaryDictionary subDict;
    337             if (noExistingDictsForThisLocale
    338                     || !dictionaryGroupForLocale.hasDict(subDictType, account)) {
    339                 // Create a new dictionary.
    340                 subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */,
    341                         dictNamePrefix, account);
    342             } else {
    343                 // Reuse the existing dictionary, and don't close it at the end
    344                 subDict = dictionaryGroupForLocale.getSubDict(subDictType);
    345                 dictTypesToCleanupForLocale.remove(subDictType);
    346             }
    347             subDicts.put(subDictType, subDict);
    348         }
    349         DictionaryGroup newDictionaryGroup =
    350                 new DictionaryGroup(newLocale, mainDict, account, subDicts);
    351 
    352         // Replace Dictionaries.
    353         final DictionaryGroup oldDictionaryGroup;
    354         synchronized (mLock) {
    355             oldDictionaryGroup = mDictionaryGroup;
    356             mDictionaryGroup = newDictionaryGroup;
    357             if (hasAtLeastOneUninitializedMainDictionary()) {
    358                 asyncReloadUninitializedMainDictionaries(context, newLocale, listener);
    359             }
    360         }
    361         if (listener != null) {
    362             listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
    363         }
    364 
    365         // Clean up old dictionaries.
    366         for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) {
    367             final ArrayList<String> dictTypesToCleanUp =
    368                     existingDictionariesToCleanup.get(localeToCleanUp);
    369             final DictionaryGroup dictionarySetToCleanup =
    370                     findDictionaryGroupWithLocale(oldDictionaryGroup, localeToCleanUp);
    371             for (final String dictType : dictTypesToCleanUp) {
    372                 dictionarySetToCleanup.closeDict(dictType);
    373             }
    374         }
    375 
    376         if (mValidSpellingWordWriteCache != null) {
    377             mValidSpellingWordWriteCache.evictAll();
    378         }
    379     }
    380 
    381     private void asyncReloadUninitializedMainDictionaries(final Context context,
    382             final Locale locale, final DictionaryInitializationListener listener) {
    383         final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1);
    384         mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary;
    385         ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() {
    386             @Override
    387             public void run() {
    388                 doReloadUninitializedMainDictionaries(
    389                         context, locale, listener, latchForWaitingLoadingMainDictionary);
    390             }
    391         });
    392     }
    393 
    394     void doReloadUninitializedMainDictionaries(final Context context, final Locale locale,
    395             final DictionaryInitializationListener listener,
    396             final CountDownLatch latchForWaitingLoadingMainDictionary) {
    397         final DictionaryGroup dictionaryGroup =
    398                 findDictionaryGroupWithLocale(mDictionaryGroup, locale);
    399         if (null == dictionaryGroup) {
    400             // This should never happen, but better safe than crashy
    401             Log.w(TAG, "Expected a dictionary group for " + locale + " but none found");
    402             return;
    403         }
    404         final Dictionary mainDict =
    405                 DictionaryFactory.createMainDictionaryFromManager(context, locale);
    406         synchronized (mLock) {
    407             if (locale.equals(dictionaryGroup.mLocale)) {
    408                 dictionaryGroup.setMainDict(mainDict);
    409             } else {
    410                 // Dictionary facilitator has been reset for another locale.
    411                 mainDict.close();
    412             }
    413         }
    414         if (listener != null) {
    415             listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary());
    416         }
    417         latchForWaitingLoadingMainDictionary.countDown();
    418     }
    419 
    420     @UsedForTesting
    421     public void resetDictionariesForTesting(final Context context, final Locale locale,
    422             final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles,
    423             final Map<String, Map<String, String>> additionalDictAttributes,
    424             @Nullable final String account) {
    425         Dictionary mainDictionary = null;
    426         final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>();
    427 
    428         for (final String dictType : dictionaryTypes) {
    429             if (dictType.equals(Dictionary.TYPE_MAIN)) {
    430                 mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context,
    431                         locale);
    432             } else {
    433                 final File dictFile = dictionaryFiles.get(dictType);
    434                 final ExpandableBinaryDictionary dict = getSubDict(
    435                         dictType, context, locale, dictFile, "" /* dictNamePrefix */, account);
    436                 if (additionalDictAttributes.containsKey(dictType)) {
    437                     dict.clearAndFlushDictionaryWithAdditionalAttributes(
    438                             additionalDictAttributes.get(dictType));
    439                 }
    440                 if (dict == null) {
    441                     throw new RuntimeException("Unknown dictionary type: " + dictType);
    442                 }
    443                 dict.reloadDictionaryIfRequired();
    444                 dict.waitAllTasksForTests();
    445                 subDicts.put(dictType, dict);
    446             }
    447         }
    448         mDictionaryGroup = new DictionaryGroup(locale, mainDictionary, account, subDicts);
    449     }
    450 
    451     public void closeDictionaries() {
    452         final DictionaryGroup dictionaryGroupToClose;
    453         synchronized (mLock) {
    454             dictionaryGroupToClose = mDictionaryGroup;
    455             mDictionaryGroup = new DictionaryGroup();
    456         }
    457         for (final String dictType : ALL_DICTIONARY_TYPES) {
    458             dictionaryGroupToClose.closeDict(dictType);
    459         }
    460     }
    461 
    462     @UsedForTesting
    463     public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) {
    464         return mDictionaryGroup.getSubDict(dictName);
    465     }
    466 
    467     // The main dictionaries are loaded asynchronously.  Don't cache the return value
    468     // of these methods.
    469     public boolean hasAtLeastOneInitializedMainDictionary() {
    470         final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN);
    471         if (mainDict != null && mainDict.isInitialized()) {
    472             return true;
    473         }
    474         return false;
    475     }
    476 
    477     public boolean hasAtLeastOneUninitializedMainDictionary() {
    478         final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN);
    479         if (mainDict == null || !mainDict.isInitialized()) {
    480             return true;
    481         }
    482         return false;
    483     }
    484 
    485     public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit)
    486             throws InterruptedException {
    487         mLatchForWaitingLoadingMainDictionaries.await(timeout, unit);
    488     }
    489 
    490     @UsedForTesting
    491     public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit)
    492             throws InterruptedException {
    493         waitForLoadingMainDictionaries(timeout, unit);
    494         for (final ExpandableBinaryDictionary dict : mDictionaryGroup.mSubDictMap.values()) {
    495             dict.waitAllTasksForTests();
    496         }
    497     }
    498 
    499     public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized,
    500             @Nonnull final NgramContext ngramContext, final long timeStampInSeconds,
    501             final boolean blockPotentiallyOffensive) {
    502         // Update the spelling cache before learning. Words that are not yet added to user history
    503         // and appear in no other language model are not considered valid.
    504         putWordIntoValidSpellingWordCache("addToUserHistory", suggestion);
    505 
    506         final String[] words = suggestion.split(Constants.WORD_SEPARATOR);
    507         NgramContext ngramContextForCurrentWord = ngramContext;
    508         for (int i = 0; i < words.length; i++) {
    509             final String currentWord = words[i];
    510             final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false;
    511             addWordToUserHistory(mDictionaryGroup, ngramContextForCurrentWord, currentWord,
    512                     wasCurrentWordAutoCapitalized, (int) timeStampInSeconds,
    513                     blockPotentiallyOffensive);
    514             ngramContextForCurrentWord =
    515                     ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord));
    516         }
    517     }
    518 
    519     private void putWordIntoValidSpellingWordCache(
    520             @Nonnull final String caller,
    521             @Nonnull final String originalWord) {
    522         if (mValidSpellingWordWriteCache == null) {
    523             return;
    524         }
    525 
    526         final String lowerCaseWord = originalWord.toLowerCase(getLocale());
    527         final boolean lowerCaseValid = isValidSpellingWord(lowerCaseWord);
    528         mValidSpellingWordWriteCache.put(lowerCaseWord, lowerCaseValid);
    529 
    530         final String capitalWord =
    531                 StringUtils.capitalizeFirstAndDowncaseRest(originalWord, getLocale());
    532         final boolean capitalValid;
    533         if (lowerCaseValid) {
    534             // The lower case form of the word is valid, so the upper case must be valid.
    535             capitalValid = true;
    536         } else {
    537             capitalValid = isValidSpellingWord(capitalWord);
    538         }
    539         mValidSpellingWordWriteCache.put(capitalWord, capitalValid);
    540     }
    541 
    542     private void addWordToUserHistory(final DictionaryGroup dictionaryGroup,
    543             final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized,
    544             final int timeStampInSeconds, final boolean blockPotentiallyOffensive) {
    545         final ExpandableBinaryDictionary userHistoryDictionary =
    546                 dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY);
    547         if (userHistoryDictionary == null || !isForLocale(userHistoryDictionary.mLocale)) {
    548             return;
    549         }
    550         final int maxFreq = getFrequency(word);
    551         if (maxFreq == 0 && blockPotentiallyOffensive) {
    552             return;
    553         }
    554         final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale);
    555         final String secondWord;
    556         if (wasAutoCapitalized) {
    557             if (isValidSuggestionWord(word) && !isValidSuggestionWord(lowerCasedWord)) {
    558                 // If the word was auto-capitalized and exists only as a capitalized word in the
    559                 // dictionary, then we must not downcase it before registering it. For example,
    560                 // the name of the contacts in start-of-sentence position would come here with the
    561                 // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version
    562                 // of that contact's name which would end up popping in suggestions.
    563                 secondWord = word;
    564             } else {
    565                 // If however the word is not in the dictionary, or exists as a lower-case word
    566                 // only, then we consider that was a lower-case word that had been auto-capitalized.
    567                 secondWord = lowerCasedWord;
    568             }
    569         } else {
    570             // HACK: We'd like to avoid adding the capitalized form of common words to the User
    571             // History dictionary in order to avoid suggesting them until the dictionary
    572             // consolidation is done.
    573             // TODO: Remove this hack when ready.
    574             final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN,
    575                     null /* account */) ?
    576                     dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) :
    577                     Dictionary.NOT_A_PROBABILITY;
    578             if (maxFreq < lowerCaseFreqInMainDict
    579                     && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) {
    580                 // Use lower cased word as the word can be a distracter of the popular word.
    581                 secondWord = lowerCasedWord;
    582             } else {
    583                 secondWord = word;
    584             }
    585         }
    586         // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid".
    587         // We don't add words with 0-frequency (assuming they would be profanity etc.).
    588         final boolean isValid = maxFreq > 0;
    589         UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, secondWord,
    590                 isValid, timeStampInSeconds);
    591     }
    592 
    593     private void removeWord(final String dictName, final String word) {
    594         final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName);
    595         if (dictionary != null) {
    596             dictionary.removeUnigramEntryDynamically(word);
    597         }
    598     }
    599 
    600     @Override
    601     public void unlearnFromUserHistory(final String word,
    602             @Nonnull final NgramContext ngramContext, final long timeStampInSeconds,
    603             final int eventType) {
    604         // TODO: Decide whether or not to remove the word on EVENT_BACKSPACE.
    605         if (eventType != Constants.EVENT_BACKSPACE) {
    606             removeWord(Dictionary.TYPE_USER_HISTORY, word);
    607         }
    608 
    609         // Update the spelling cache after unlearning. Words that are removed from user history
    610         // and appear in no other language model are not considered valid.
    611         putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.toLowerCase());
    612     }
    613 
    614     // TODO: Revise the way to fusion suggestion results.
    615     @Override
    616     @Nonnull public SuggestionResults getSuggestionResults(ComposedData composedData,
    617             NgramContext ngramContext, @Nonnull final Keyboard keyboard,
    618             SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId,
    619             int inputStyle) {
    620         long proximityInfoHandle = keyboard.getProximityInfo().getNativeProximityInfo();
    621         final SuggestionResults suggestionResults = new SuggestionResults(
    622                 SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext(),
    623                 false /* firstSuggestionExceedsConfidenceThreshold */);
    624         final float[] weightOfLangModelVsSpatialModel =
    625                 new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL };
    626         for (final String dictType : ALL_DICTIONARY_TYPES) {
    627             final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
    628             if (null == dictionary) continue;
    629             final float weightForLocale = composedData.mIsBatchMode
    630                     ? mDictionaryGroup.mWeightForGesturingInLocale
    631                     : mDictionaryGroup.mWeightForTypingInLocale;
    632             final ArrayList<SuggestedWordInfo> dictionarySuggestions =
    633                     dictionary.getSuggestions(composedData, ngramContext,
    634                             proximityInfoHandle, settingsValuesForSuggestion, sessionId,
    635                             weightForLocale, weightOfLangModelVsSpatialModel);
    636             if (null == dictionarySuggestions) continue;
    637             suggestionResults.addAll(dictionarySuggestions);
    638             if (null != suggestionResults.mRawSuggestions) {
    639                 suggestionResults.mRawSuggestions.addAll(dictionarySuggestions);
    640             }
    641         }
    642         return suggestionResults;
    643     }
    644 
    645     public boolean isValidSpellingWord(final String word) {
    646         if (mValidSpellingWordReadCache != null) {
    647             final Boolean cachedValue = mValidSpellingWordReadCache.get(word);
    648             if (cachedValue != null) {
    649                 return cachedValue;
    650             }
    651         }
    652 
    653         return isValidWord(word, ALL_DICTIONARY_TYPES);
    654     }
    655 
    656     public boolean isValidSuggestionWord(final String word) {
    657         return isValidWord(word, ALL_DICTIONARY_TYPES);
    658     }
    659 
    660     private boolean isValidWord(final String word, final String[] dictionariesToCheck) {
    661         if (TextUtils.isEmpty(word)) {
    662             return false;
    663         }
    664         if (mDictionaryGroup.mLocale == null) {
    665             return false;
    666         }
    667         for (final String dictType : dictionariesToCheck) {
    668             final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
    669             // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and
    670             // would be immutable once it's finished initializing, but concretely a null test is
    671             // probably good enough for the time being.
    672             if (null == dictionary) continue;
    673             if (dictionary.isValidWord(word)) {
    674                 return true;
    675             }
    676         }
    677         return false;
    678     }
    679 
    680     private int getFrequency(final String word) {
    681         if (TextUtils.isEmpty(word)) {
    682             return Dictionary.NOT_A_PROBABILITY;
    683         }
    684         int maxFreq = Dictionary.NOT_A_PROBABILITY;
    685         for (final String dictType : ALL_DICTIONARY_TYPES) {
    686             final Dictionary dictionary = mDictionaryGroup.getDict(dictType);
    687             if (dictionary == null) continue;
    688             final int tempFreq = dictionary.getFrequency(word);
    689             if (tempFreq >= maxFreq) {
    690                 maxFreq = tempFreq;
    691             }
    692         }
    693         return maxFreq;
    694     }
    695 
    696     private boolean clearSubDictionary(final String dictName) {
    697         final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName);
    698         if (dictionary == null) {
    699             return false;
    700         }
    701         dictionary.clear();
    702         return true;
    703     }
    704 
    705     @Override
    706     public boolean clearUserHistoryDictionary(final Context context) {
    707         return clearSubDictionary(Dictionary.TYPE_USER_HISTORY);
    708     }
    709 
    710     @Override
    711     public void dumpDictionaryForDebug(final String dictName) {
    712         final ExpandableBinaryDictionary dictToDump = mDictionaryGroup.getSubDict(dictName);
    713         if (dictToDump == null) {
    714             Log.e(TAG, "Cannot dump " + dictName + ". "
    715                     + "The dictionary is not being used for suggestion or cannot be dumped.");
    716             return;
    717         }
    718         dictToDump.dumpAllWordsForDebug();
    719     }
    720 
    721     @Override
    722     @Nonnull public List<DictionaryStats> getDictionaryStats(final Context context) {
    723         final ArrayList<DictionaryStats> statsOfEnabledSubDicts = new ArrayList<>();
    724         for (final String dictType : DYNAMIC_DICTIONARY_TYPES) {
    725             final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictType);
    726             if (dictionary == null) continue;
    727             statsOfEnabledSubDicts.add(dictionary.getDictionaryStats());
    728         }
    729         return statsOfEnabledSubDicts;
    730     }
    731 
    732     @Override
    733     public String dump(final Context context) {
    734         return "";
    735     }
    736 }
    737