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 {
DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class)80         DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class);
DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class)81         DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class);
DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.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
setValidSpellingWordReadCache(final LruCache<String, Boolean> cache)93     public void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache) {
94         mValidSpellingWordReadCache = cache;
95     }
96 
97     @Override
setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache)98     public void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache) {
99         mValidSpellingWordWriteCache = cache;
100     }
101 
102     @Override
isForLocale(final Locale locale)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      */
isForAccount(@ullable final String account)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 
DictionaryGroup()148         public DictionaryGroup() {
149             this(null /* locale */, null /* mainDict */, null /* account */,
150                     Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */);
151         }
152 
DictionaryGroup(@ullable final Locale locale, @Nullable final Dictionary mainDict, @Nullable final String account, final Map<String, ExpandableBinaryDictionary> subDicts)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 
setSubDict(final String dictType, final ExpandableBinaryDictionary dict)166         private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) {
167             if (dict != null) {
168                 mSubDictMap.put(dictType, dict);
169             }
170         }
171 
setMainDict(final Dictionary mainDict)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 
getDict(final String dictType)181         public Dictionary getDict(final String dictType) {
182             if (Dictionary.TYPE_MAIN.equals(dictType)) {
183                 return mMainDict;
184             }
185             return getSubDict(dictType);
186         }
187 
getSubDict(final String dictType)188         public ExpandableBinaryDictionary getSubDict(final String dictType) {
189             return mSubDictMap.get(dictType);
190         }
191 
hasDict(final String dictType, @Nullable final String account)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 
closeDict(final String dictType)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 
DictionaryFacilitatorImpl()219     public DictionaryFacilitatorImpl() {
220     }
221 
222     @Override
onStartInput()223     public void onStartInput() {
224     }
225 
226     @Override
onFinishInput(Context context)227     public void onFinishInput(Context context) {
228     }
229 
230     @Override
isActive()231     public boolean isActive() {
232         return mDictionaryGroup.mLocale != null;
233     }
234 
235     @Override
getLocale()236     public Locale getLocale() {
237         return mDictionaryGroup.mLocale;
238     }
239 
240     @Override
usesContacts()241     public boolean usesContacts() {
242         return mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTACTS) != null;
243     }
244 
245     @Override
getAccount()246     public String getAccount() {
247         return null;
248     }
249 
250     @Nullable
getSubDict(final String dictType, final Context context, final Locale locale, final File dictFile, final String dictNamePrefix, @Nullable final String account)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
findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup, final Locale locale)273     static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup,
274             final Locale locale) {
275         return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null;
276     }
277 
278     @Override
resetDictionaries( final Context context, final Locale newLocale, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, @Nullable final String account, final String dictNamePrefix, @Nullable final DictionaryInitializationListener listener)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 
asyncReloadUninitializedMainDictionaries(final Context context, final Locale locale, final DictionaryInitializationListener listener)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 
doReloadUninitializedMainDictionaries(final Context context, final Locale locale, final DictionaryInitializationListener listener, final CountDownLatch latchForWaitingLoadingMainDictionary)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
resetDictionariesForTesting(final Context context, final Locale locale, final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, final Map<String, Map<String, String>> additionalDictAttributes, @Nullable final String account)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 
closeDictionaries()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
getSubDictForTesting(final String dictName)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.
hasAtLeastOneInitializedMainDictionary()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 
hasAtLeastOneUninitializedMainDictionary()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 
waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit)485     public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit)
486             throws InterruptedException {
487         mLatchForWaitingLoadingMainDictionaries.await(timeout, unit);
488     }
489 
490     @UsedForTesting
waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit)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 
addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, final boolean blockPotentiallyOffensive)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 
putWordIntoValidSpellingWordCache( @onnull final String caller, @Nonnull final String originalWord)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 
addWordToUserHistory(final DictionaryGroup dictionaryGroup, final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized, final int timeStampInSeconds, final boolean blockPotentiallyOffensive)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 
removeWord(final String dictName, final String word)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
unlearnFromUserHistory(final String word, @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, final int eventType)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
getSuggestionResults(ComposedData composedData, NgramContext ngramContext, @Nonnull final Keyboard keyboard, SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId, int inputStyle)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 
isValidSpellingWord(final String word)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 
isValidSuggestionWord(final String word)656     public boolean isValidSuggestionWord(final String word) {
657         return isValidWord(word, ALL_DICTIONARY_TYPES);
658     }
659 
isValidWord(final String word, final String[] dictionariesToCheck)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 
getFrequency(final String word)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 
clearSubDictionary(final String dictName)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
clearUserHistoryDictionary(final Context context)706     public boolean clearUserHistoryDictionary(final Context context) {
707         return clearSubDictionary(Dictionary.TYPE_USER_HISTORY);
708     }
709 
710     @Override
dumpDictionaryForDebug(final String dictName)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
getDictionaryStats(final Context context)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
dump(final Context context)733     public String dump(final Context context) {
734         return "";
735     }
736 }
737