/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.inputmethod.latin; import android.content.Context; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.ProximityInfo; import com.android.inputmethod.latin.ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback; import com.android.inputmethod.latin.NgramContext.WordInfo; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import com.android.inputmethod.latin.personalization.ContextualDictionary; import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; import com.android.inputmethod.latin.personalization.PersonalizationDictionary; import com.android.inputmethod.latin.personalization.UserHistoryDictionary; import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; import com.android.inputmethod.latin.settings.SpacingAndPunctuations; import com.android.inputmethod.latin.utils.DistracterFilter; import com.android.inputmethod.latin.utils.DistracterFilterCheckingExactMatchesAndSuggestions; import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; import com.android.inputmethod.latin.utils.ExecutorUtils; import com.android.inputmethod.latin.utils.SuggestionResults; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; // TODO: Consolidate dictionaries in native code. public class DictionaryFacilitator { public static final String TAG = DictionaryFacilitator.class.getSimpleName(); // HACK: This threshold is being used when adding a capitalized entry in the User History // dictionary. private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; private DictionaryGroup[] mDictionaryGroups = new DictionaryGroup[] { new DictionaryGroup() }; private DictionaryGroup mMostProbableDictionaryGroup = mDictionaryGroups[0]; private boolean mIsUserDictEnabled = false; private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0); // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. private final Object mLock = new Object(); private final DistracterFilter mDistracterFilter; private final PersonalizationHelperForDictionaryFacilitator mPersonalizationHelper; private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = new String[] { Dictionary.TYPE_MAIN, Dictionary.TYPE_USER_HISTORY, Dictionary.TYPE_PERSONALIZATION, Dictionary.TYPE_USER, Dictionary.TYPE_CONTACTS, Dictionary.TYPE_CONTEXTUAL }; public static final Map> DICT_TYPE_TO_CLASS = new HashMap<>(); static { DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class); DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_PERSONALIZATION, PersonalizationDictionary.class); DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class); DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class); DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTEXTUAL, ContextualDictionary.class); } private static final String DICT_FACTORY_METHOD_NAME = "getDictionary"; private static final Class[] DICT_FACTORY_METHOD_ARG_TYPES = new Class[] { Context.class, Locale.class, File.class, String.class }; private static final String[] SUB_DICT_TYPES = Arrays.copyOfRange(DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS, 1 /* start */, DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); /** * Returns whether this facilitator is exactly for this list of locales. * @param locales the list of locales to test against * @return true if this facilitator handles exactly this list of locales, false otherwise */ public boolean isForLocales(final Locale[] locales) { if (locales.length != mDictionaryGroups.length) { return false; } for (final Locale locale : locales) { boolean found = false; for (final DictionaryGroup group : mDictionaryGroups) { if (locale.equals(group.mLocale)) { found = true; break; } } if (!found) { return false; } } return true; } /** * A group of dictionaries that work together for a single language. */ private static class DictionaryGroup { // TODO: Run evaluation to determine a reasonable value for these constants. The current // values are ad-hoc and chosen without any particular care or methodology. public static final float WEIGHT_FOR_MOST_PROBABLE_LANGUAGE = 1.0f; public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f; public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f; public final Locale mLocale; private Dictionary mMainDict; public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; public final ConcurrentHashMap mSubDictMap = new ConcurrentHashMap<>(); public DictionaryGroup() { mLocale = null; } public DictionaryGroup(final Locale locale, final Dictionary mainDict, final Map subDicts) { mLocale = locale; // The main dictionary can be asynchronously loaded. setMainDict(mainDict); for (final Map.Entry entry : subDicts.entrySet()) { setSubDict(entry.getKey(), entry.getValue()); } } private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) { if (dict != null) { mSubDictMap.put(dictType, dict); } } public void setMainDict(final Dictionary mainDict) { // Close old dictionary if exists. Main dictionary can be assigned multiple times. final Dictionary oldDict = mMainDict; mMainDict = mainDict; if (oldDict != null && mainDict != oldDict) { oldDict.close(); } } public Dictionary getDict(final String dictType) { if (Dictionary.TYPE_MAIN.equals(dictType)) { return mMainDict; } else { return getSubDict(dictType); } } public ExpandableBinaryDictionary getSubDict(final String dictType) { return mSubDictMap.get(dictType); } public boolean hasDict(final String dictType) { if (Dictionary.TYPE_MAIN.equals(dictType)) { return mMainDict != null; } else { return mSubDictMap.containsKey(dictType); } } public void closeDict(final String dictType) { final Dictionary dict; if (Dictionary.TYPE_MAIN.equals(dictType)) { dict = mMainDict; } else { dict = mSubDictMap.remove(dictType); } if (dict != null) { dict.close(); } } } public interface DictionaryInitializationListener { public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); } public DictionaryFacilitator() { mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; mPersonalizationHelper = null; } public DictionaryFacilitator(final Context context) { mDistracterFilter = new DistracterFilterCheckingExactMatchesAndSuggestions(context); mPersonalizationHelper = new PersonalizationHelperForDictionaryFacilitator(context, mDistracterFilter); } public void updateEnabledSubtypes(final List enabledSubtypes) { mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); mPersonalizationHelper.updateEnabledSubtypes(enabledSubtypes); } // TODO: remove this, it's confusing with seamless multiple language switching public void setIsMonolingualUser(final boolean isMonolingualUser) { mPersonalizationHelper.setIsMonolingualUser(isMonolingualUser); } public boolean isActive() { return null != mDictionaryGroups[0].mLocale; } /** * Returns the most probable locale among all currently active locales. BE CAREFUL using this. * * DO NOT USE THIS just because it's convenient. Use it when it's correct, for example when * choosing what dictionary to put a word in, or when changing the capitalization of a typed * string. * @return the most probable locale */ public Locale getMostProbableLocale() { return getDictionaryGroupForMostProbableLanguage().mLocale; } public Locale[] getLocales() { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; final Locale[] locales = new Locale[dictionaryGroups.length]; for (int i = 0; i < dictionaryGroups.length; ++i) { locales[i] = dictionaryGroups[i].mLocale; } return locales; } private DictionaryGroup getDictionaryGroupForMostProbableLanguage() { return mMostProbableDictionaryGroup; } public void switchMostProbableLanguage(final Locale locale) { if (null == locale) { // In many cases, there is no locale to a committed word. For example, a typed word // that does not auto-correct has no locale. In this case we simply do not change // the most probable language. return; } final DictionaryGroup newMostProbableDictionaryGroup = findDictionaryGroupWithLocale(mDictionaryGroups, locale); mMostProbableDictionaryGroup.mWeightForTypingInLocale = DictionaryGroup.WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE; mMostProbableDictionaryGroup.mWeightForGesturingInLocale = DictionaryGroup.WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE; newMostProbableDictionaryGroup.mWeightForTypingInLocale = DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; newMostProbableDictionaryGroup.mWeightForGesturingInLocale = DictionaryGroup.WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; mMostProbableDictionaryGroup = newMostProbableDictionaryGroup; } private static ExpandableBinaryDictionary getSubDict(final String dictType, final Context context, final Locale locale, final File dictFile, final String dictNamePrefix) { final Class dictClass = DICT_TYPE_TO_CLASS.get(dictType); if (dictClass == null) { return null; } try { final Method factoryMethod = dictClass.getMethod(DICT_FACTORY_METHOD_NAME, DICT_FACTORY_METHOD_ARG_TYPES); final Object dict = factoryMethod.invoke(null /* obj */, new Object[] { context, locale, dictFile, dictNamePrefix }); return (ExpandableBinaryDictionary) dict; } catch (final NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { Log.e(TAG, "Cannot create dictionary: " + dictType, e); return null; } } public void resetDictionaries(final Context context, final Locale[] newLocales, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener) { resetDictionariesWithDictNamePrefix(context, newLocales, useContactsDict, usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */); } private DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup[] dictionaryGroups, final Locale locale) { for (int i = 0; i < dictionaryGroups.length; ++i) { if (locale.equals(dictionaryGroups[i].mLocale)) { return dictionaryGroups[i]; } } return null; } public void resetDictionariesWithDictNamePrefix(final Context context, final Locale[] newLocales, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener, final String dictNamePrefix) { final HashMap> existingDictsToCleanup = new HashMap<>(); // TODO: Make subDictTypesToUse configurable by resource or a static final list. final HashSet subDictTypesToUse = new HashSet<>(); subDictTypesToUse.add(Dictionary.TYPE_USER); if (useContactsDict) { subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); } if (usePersonalizedDicts) { subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); } // Gather all dictionaries. We'll remove them from the list to clean up later. for (final Locale newLocale : newLocales) { final ArrayList dictsForLocale = new ArrayList<>(); existingDictsToCleanup.put(newLocale, dictsForLocale); final DictionaryGroup currentDictionaryGroupForLocale = findDictionaryGroupWithLocale(mDictionaryGroups, newLocale); if (null == currentDictionaryGroupForLocale) { continue; } for (final String dictType : SUB_DICT_TYPES) { if (currentDictionaryGroupForLocale.hasDict(dictType)) { dictsForLocale.add(dictType); } } if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) { dictsForLocale.add(Dictionary.TYPE_MAIN); } } final DictionaryGroup[] newDictionaryGroups = new DictionaryGroup[newLocales.length]; for (int i = 0; i < newLocales.length; ++i) { final Locale newLocale = newLocales[i]; final DictionaryGroup dictionaryGroupForLocale = findDictionaryGroupWithLocale(mDictionaryGroups, newLocale); final ArrayList dictsToCleanupForLocale = existingDictsToCleanup.get(newLocale); final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); final Dictionary mainDict; if (forceReloadMainDictionary || noExistingDictsForThisLocale || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN)) { mainDict = null; } else { mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); dictsToCleanupForLocale.remove(Dictionary.TYPE_MAIN); } final Map subDicts = new HashMap<>(); for (final String subDictType : subDictTypesToUse) { final ExpandableBinaryDictionary subDict; if (noExistingDictsForThisLocale || !dictionaryGroupForLocale.hasDict(subDictType)) { // Create a new dictionary. subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, dictNamePrefix); } else { // Reuse the existing dictionary, and don't close it at the end subDict = dictionaryGroupForLocale.getSubDict(subDictType); dictsToCleanupForLocale.remove(subDictType); } subDicts.put(subDictType, subDict); } newDictionaryGroups[i] = new DictionaryGroup(newLocale, mainDict, subDicts); } // Replace Dictionaries. final DictionaryGroup[] oldDictionaryGroups; synchronized (mLock) { oldDictionaryGroups = mDictionaryGroups; mDictionaryGroups = newDictionaryGroups; mMostProbableDictionaryGroup = newDictionaryGroups[0]; mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); if (hasAtLeastOneUninitializedMainDictionary()) { asyncReloadUninitializedMainDictionaries(context, newLocales, listener); } } if (listener != null) { listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); } // Clean up old dictionaries. for (final Locale localeToCleanUp : existingDictsToCleanup.keySet()) { final ArrayList dictTypesToCleanUp = existingDictsToCleanup.get(localeToCleanUp); final DictionaryGroup dictionarySetToCleanup = findDictionaryGroupWithLocale(oldDictionaryGroups, localeToCleanUp); for (final String dictType : dictTypesToCleanUp) { dictionarySetToCleanup.closeDict(dictType); } } } private void asyncReloadUninitializedMainDictionaries(final Context context, final Locale[] locales, final DictionaryInitializationListener listener) { final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary; ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { @Override public void run() { for (final Locale locale : locales) { final DictionaryGroup dictionaryGroup = findDictionaryGroupWithLocale(mDictionaryGroups, locale); if (null == dictionaryGroup) { // This should never happen, but better safe than crashy Log.w(TAG, "Expected a dictionary group for " + locale + " but none found"); continue; } final Dictionary mainDict = DictionaryFactory.createMainDictionaryFromManager(context, locale); synchronized (mLock) { if (locale.equals(dictionaryGroup.mLocale)) { dictionaryGroup.setMainDict(mainDict); } else { // Dictionary facilitator has been reset for another locale. mainDict.close(); } } } if (listener != null) { listener.onUpdateMainDictionaryAvailability( hasAtLeastOneInitializedMainDictionary()); } latchForWaitingLoadingMainDictionary.countDown(); } }); } @UsedForTesting public void resetDictionariesForTesting(final Context context, final Locale[] locales, final ArrayList dictionaryTypes, final HashMap dictionaryFiles, final Map> additionalDictAttributes) { Dictionary mainDictionary = null; final Map subDicts = new HashMap<>(); final DictionaryGroup[] dictionaryGroups = new DictionaryGroup[locales.length]; for (int i = 0; i < locales.length; ++i) { final Locale locale = locales[i]; for (final String dictType : dictionaryTypes) { if (dictType.equals(Dictionary.TYPE_MAIN)) { mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, locale); } else { final File dictFile = dictionaryFiles.get(dictType); final ExpandableBinaryDictionary dict = getSubDict( dictType, context, locale, dictFile, "" /* dictNamePrefix */); if (additionalDictAttributes.containsKey(dictType)) { dict.clearAndFlushDictionaryWithAdditionalAttributes( additionalDictAttributes.get(dictType)); } if (dict == null) { throw new RuntimeException("Unknown dictionary type: " + dictType); } dict.reloadDictionaryIfRequired(); dict.waitAllTasksForTests(); subDicts.put(dictType, dict); } } dictionaryGroups[i] = new DictionaryGroup(locale, mainDictionary, subDicts); } mDictionaryGroups = dictionaryGroups; mMostProbableDictionaryGroup = dictionaryGroups[0]; } public void closeDictionaries() { final DictionaryGroup[] dictionaryGroups; synchronized (mLock) { dictionaryGroups = mDictionaryGroups; mMostProbableDictionaryGroup = new DictionaryGroup(); mDictionaryGroups = new DictionaryGroup[] { mMostProbableDictionaryGroup }; } for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { dictionaryGroup.closeDict(dictType); } } mDistracterFilter.close(); if (mPersonalizationHelper != null) { mPersonalizationHelper.close(); } } @UsedForTesting public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { return mMostProbableDictionaryGroup.getSubDict(dictName); } // The main dictionaries are loaded asynchronously. Don't cache the return value // of these methods. public boolean hasAtLeastOneInitializedMainDictionary() { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN); if (mainDict != null && mainDict.isInitialized()) { return true; } } return false; } public boolean hasAtLeastOneUninitializedMainDictionary() { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { final Dictionary mainDict = dictionaryGroup.getDict(Dictionary.TYPE_MAIN); if (mainDict == null || !mainDict.isInitialized()) { return true; } } return false; } public boolean hasPersonalizationDictionary() { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { if (dictionaryGroup.hasDict(Dictionary.TYPE_PERSONALIZATION)) { return true; } } return false; } public void flushPersonalizationDictionary() { final HashSet personalizationDictsUsedForSuggestion = new HashSet<>(); final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { final ExpandableBinaryDictionary personalizationDictUsedForSuggestion = dictionaryGroup.getSubDict(Dictionary.TYPE_PERSONALIZATION); personalizationDictsUsedForSuggestion.add(personalizationDictUsedForSuggestion); } mPersonalizationHelper.flushPersonalizationDictionariesToUpdate( personalizationDictsUsedForSuggestion); mDistracterFilter.close(); } public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit) throws InterruptedException { mLatchForWaitingLoadingMainDictionaries.await(timeout, unit); } @UsedForTesting public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) throws InterruptedException { waitForLoadingMainDictionaries(timeout, unit); final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { for (final ExpandableBinaryDictionary dict : dictionaryGroup.mSubDictMap.values()) { dict.waitAllTasksForTests(); } } } public boolean isUserDictionaryEnabled() { return mIsUserDictEnabled; } public void addWordToUserDictionary(final Context context, final String word) { final Locale locale = getMostProbableLocale(); if (locale == null) { return; } // TODO: add a toast telling what language this is being added to? UserBinaryDictionary.addWordToUserDictionary(context, locale, word); } public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, final NgramContext ngramContext, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { final DictionaryGroup dictionaryGroup = getDictionaryGroupForMostProbableLanguage(); final String[] words = suggestion.split(Constants.WORD_SEPARATOR); NgramContext ngramContextForCurrentWord = ngramContext; for (int i = 0; i < words.length; i++) { final String currentWord = words[i]; final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; addWordToUserHistory(dictionaryGroup, ngramContextForCurrentWord, currentWord, wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); ngramContextForCurrentWord = ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); } } private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized, final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { final ExpandableBinaryDictionary userHistoryDictionary = dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY); if (userHistoryDictionary == null) { return; } final int maxFreq = getFrequency(word); if (maxFreq == 0 && blockPotentiallyOffensive) { return; } final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); final String secondWord; if (wasAutoCapitalized) { if (isValidWord(word, false /* ignoreCase */) && !isValidWord(lowerCasedWord, false /* ignoreCase */)) { // If the word was auto-capitalized and exists only as a capitalized word in the // dictionary, then we must not downcase it before registering it. For example, // the name of the contacts in start-of-sentence position would come here with the // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version // of that contact's name which would end up popping in suggestions. secondWord = word; } else { // If however the word is not in the dictionary, or exists as a lower-case word // only, then we consider that was a lower-case word that had been auto-capitalized. secondWord = lowerCasedWord; } } else { // HACK: We'd like to avoid adding the capitalized form of common words to the User // History dictionary in order to avoid suggesting them until the dictionary // consolidation is done. // TODO: Remove this hack when ready. final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN) ? dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : Dictionary.NOT_A_PROBABILITY; if (maxFreq < lowerCaseFreqInMainDict && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { // Use lower cased word as the word can be a distracter of the popular word. secondWord = lowerCasedWord; } else { secondWord = word; } } // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". // We don't add words with 0-frequency (assuming they would be profanity etc.). final boolean isValid = maxFreq > 0; UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, secondWord, isValid, timeStampInSeconds, new DistracterFilterCheckingIsInDictionary( mDistracterFilter, userHistoryDictionary)); } private void removeWord(final String dictName, final String word) { final ExpandableBinaryDictionary dictionary = getDictionaryGroupForMostProbableLanguage().getSubDict(dictName); if (dictionary != null) { dictionary.removeUnigramEntryDynamically(word); } } public void removeWordFromPersonalizedDicts(final String word) { removeWord(Dictionary.TYPE_USER_HISTORY, word); removeWord(Dictionary.TYPE_PERSONALIZATION, word); removeWord(Dictionary.TYPE_CONTEXTUAL, word); } // TODO: Revise the way to fusion suggestion results. public SuggestionResults getSuggestionResults(final WordComposer composer, final NgramContext ngramContext, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; final SuggestionResults suggestionResults = new SuggestionResults( SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext()); final float[] weightOfLangModelVsSpatialModel = new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL }; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { final Dictionary dictionary = dictionaryGroup.getDict(dictType); if (null == dictionary) continue; final float weightForLocale = composer.isBatchMode() ? dictionaryGroup.mWeightForGesturingInLocale : dictionaryGroup.mWeightForTypingInLocale; final ArrayList dictionarySuggestions = dictionary.getSuggestions(composer, ngramContext, proximityInfo, settingsValuesForSuggestion, sessionId, weightForLocale, weightOfLangModelVsSpatialModel); if (null == dictionarySuggestions) continue; suggestionResults.addAll(dictionarySuggestions); if (null != suggestionResults.mRawSuggestions) { suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); } } } return suggestionResults; } public boolean isValidWord(final String word, final boolean ignoreCase) { if (TextUtils.isEmpty(word)) { return false; } final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { if (dictionaryGroup.mLocale == null) { continue; } final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { final Dictionary dictionary = dictionaryGroup.getDict(dictType); // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and // would be immutable once it's finished initializing, but concretely a null test is // probably good enough for the time being. if (null == dictionary) continue; if (dictionary.isValidWord(word) || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) { return true; } } } return false; } private int getFrequencyInternal(final String word, final boolean isGettingMaxFrequencyOfExactMatches) { if (TextUtils.isEmpty(word)) { return Dictionary.NOT_A_PROBABILITY; } int maxFreq = Dictionary.NOT_A_PROBABILITY; final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { final Dictionary dictionary = dictionaryGroup.getDict(dictType); if (dictionary == null) continue; final int tempFreq; if (isGettingMaxFrequencyOfExactMatches) { tempFreq = dictionary.getMaxFrequencyOfExactMatches(word); } else { tempFreq = dictionary.getFrequency(word); } if (tempFreq >= maxFreq) { maxFreq = tempFreq; } } } return maxFreq; } public int getFrequency(final String word) { return getFrequencyInternal(word, false /* isGettingMaxFrequencyOfExactMatches */); } public int getMaxFrequencyOfExactMatches(final String word) { return getFrequencyInternal(word, true /* isGettingMaxFrequencyOfExactMatches */); } private void clearSubDictionary(final String dictName) { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictName); if (dictionary != null) { dictionary.clear(); } } } public void clearUserHistoryDictionary() { clearSubDictionary(Dictionary.TYPE_USER_HISTORY); } // This method gets called only when the IME receives a notification to remove the // personalization dictionary. public void clearPersonalizationDictionary() { clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); mPersonalizationHelper.clearDictionariesToUpdate(); } public void clearContextualDictionary() { clearSubDictionary(Dictionary.TYPE_CONTEXTUAL); } public void addEntriesToPersonalizationDictionary( final PersonalizationDataChunk personalizationDataChunk, final SpacingAndPunctuations spacingAndPunctuations, final AddMultipleDictionaryEntriesCallback callback) { mPersonalizationHelper.addEntriesToPersonalizationDictionariesToUpdate( getMostProbableLocale(), personalizationDataChunk, spacingAndPunctuations, callback); } @UsedForTesting public void addPhraseToContextualDictionary(final String[] phrase, final int probability, final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { // TODO: we're inserting the phrase into the dictionary for the active language. Rethink // this a bit from a theoretical point of view. final ExpandableBinaryDictionary contextualDict = getDictionaryGroupForMostProbableLanguage().getSubDict(Dictionary.TYPE_CONTEXTUAL); if (contextualDict == null) { return; } NgramContext ngramContext = NgramContext.BEGINNING_OF_SENTENCE; for (int i = 0; i < phrase.length; i++) { final String[] subPhrase = Arrays.copyOfRange(phrase, i /* start */, phrase.length); final String subPhraseStr = TextUtils.join(Constants.WORD_SEPARATOR, subPhrase); contextualDict.addUnigramEntryWithCheckingDistracter( subPhraseStr, probability, null /* shortcutTarget */, Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, false /* isNotAWord */, false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP, DistracterFilter.EMPTY_DISTRACTER_FILTER); contextualDict.addNgramEntry(ngramContext, subPhraseStr, bigramProbabilityForPhrases, BinaryDictionary.NOT_A_VALID_TIMESTAMP); if (i < phrase.length - 1) { contextualDict.addUnigramEntryWithCheckingDistracter( phrase[i], probability, null /* shortcutTarget */, Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, false /* isNotAWord */, false /* isBlacklisted */, BinaryDictionary.NOT_A_VALID_TIMESTAMP, DistracterFilter.EMPTY_DISTRACTER_FILTER); contextualDict.addNgramEntry(ngramContext, phrase[i], bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP); } ngramContext = ngramContext.getNextNgramContext(new NgramContext.WordInfo(phrase[i])); } } public void dumpDictionaryForDebug(final String dictName) { final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { final ExpandableBinaryDictionary dictToDump = dictionaryGroup.getSubDict(dictName); if (dictToDump == null) { Log.e(TAG, "Cannot dump " + dictName + ". " + "The dictionary is not being used for suggestion or cannot be dumped."); return; } dictToDump.dumpAllWordsForDebug(); } } public ArrayList> getStatsOfEnabledSubDicts() { final ArrayList> statsOfEnabledSubDicts = new ArrayList<>(); final DictionaryGroup[] dictionaryGroups = mDictionaryGroups; for (final DictionaryGroup dictionaryGroup : dictionaryGroups) { for (final String dictType : SUB_DICT_TYPES) { final ExpandableBinaryDictionary dictionary = dictionaryGroup.getSubDict(dictType); if (dictionary == null) continue; statsOfEnabledSubDicts.add(new Pair<>(dictType, dictionary.getDictionaryStats())); } } return statsOfEnabledSubDicts; } }