1/*
27 * 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
17package com.android.inputmethod.latin;
18
19import android.Manifest;
20import android.content.Context;
21import android.text.TextUtils;
22import android.util.Log;
23import android.util.LruCache;
24
25import com.android.inputmethod.annotations.UsedForTesting;
26import com.android.inputmethod.keyboard.Keyboard;
27import com.android.inputmethod.latin.NgramContext.WordInfo;
28import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
29import com.android.inputmethod.latin.common.ComposedData;
30import com.android.inputmethod.latin.common.Constants;
31import com.android.inputmethod.latin.common.StringUtils;
32import com.android.inputmethod.latin.permissions.PermissionsUtil;
33import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
34import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
35import com.android.inputmethod.latin.utils.ExecutorUtils;
36import com.android.inputmethod.latin.utils.SuggestionResults;
37
38import java.io.File;
39import java.lang.reflect.InvocationTargetException;
40import java.lang.reflect.Method;
41import java.util.ArrayList;
42import java.util.Collections;
43import java.util.HashMap;
44import java.util.HashSet;
45import java.util.List;
46import java.util.Locale;
47import java.util.Map;
48import java.util.concurrent.ConcurrentHashMap;
49import java.util.concurrent.CountDownLatch;
50import java.util.concurrent.TimeUnit;
51
52import javax.annotation.Nonnull;
53import 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 */
63public 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