diff options
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/DictionaryFacilitatorImpl.java')
-rw-r--r-- | java/src/org/kelar/inputmethod/latin/DictionaryFacilitatorImpl.java | 736 |
1 files changed, 736 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/org/kelar/inputmethod/latin/DictionaryFacilitatorImpl.java new file mode 100644 index 000000000..63c2cea4e --- /dev/null +++ b/java/src/org/kelar/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -0,0 +1,736 @@ +/* +7 * 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 org.kelar.inputmethod.latin; + +import android.Manifest; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +import android.util.LruCache; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.keyboard.Keyboard; +import org.kelar.inputmethod.latin.NgramContext.WordInfo; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.common.ComposedData; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.common.StringUtils; +import org.kelar.inputmethod.latin.permissions.PermissionsUtil; +import org.kelar.inputmethod.latin.personalization.UserHistoryDictionary; +import org.kelar.inputmethod.latin.settings.SettingsValuesForSuggestion; +import org.kelar.inputmethod.latin.utils.ExecutorUtils; +import org.kelar.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.Collections; +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; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Facilitates interaction with different kinds of dictionaries. Provides APIs + * to instantiate and select the correct dictionaries (based on language or account), + * update entries and fetch suggestions. + * + * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as + * a client for interacting with dictionaries. + */ +public class DictionaryFacilitatorImpl implements DictionaryFacilitator { + // TODO: Consolidate dictionaries in native code. + public static final String TAG = DictionaryFacilitatorImpl.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 mDictionaryGroup = new DictionaryGroup(); + private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0); + // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. + private final Object mLock = new Object(); + + public static final Map<String, Class<? extends ExpandableBinaryDictionary>> + 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_USER, UserBinaryDictionary.class); + DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.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, String.class }; + + private LruCache<String, Boolean> mValidSpellingWordReadCache; + private LruCache<String, Boolean> mValidSpellingWordWriteCache; + + @Override + public void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache) { + mValidSpellingWordReadCache = cache; + } + + @Override + public void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache) { + mValidSpellingWordWriteCache = cache; + } + + @Override + public boolean isForLocale(final Locale locale) { + return locale != null && locale.equals(mDictionaryGroup.mLocale); + } + + /** + * Returns whether this facilitator is exactly for this account. + * + * @param account the account to test against. + */ + public boolean isForAccount(@Nullable final String account) { + return TextUtils.equals(mDictionaryGroup.mAccount, account); + } + + /** + * A group of dictionaries that work together for a single language. + */ + private static class DictionaryGroup { + // TODO: Add null analysis annotations. + // 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; + + /** + * The locale associated with the dictionary group. + */ + @Nullable public final Locale mLocale; + + /** + * The user account associated with the dictionary group. + */ + @Nullable public final String mAccount; + + @Nullable private Dictionary mMainDict; + // Confidence that the most probable language is actually the language the user is + // typing in. For now, this is simply the number of times a word from this language + // has been committed in a row. + private int mConfidence = 0; + + public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; + public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = + new ConcurrentHashMap<>(); + + public DictionaryGroup() { + this(null /* locale */, null /* mainDict */, null /* account */, + Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */); + } + + public DictionaryGroup(@Nullable final Locale locale, + @Nullable final Dictionary mainDict, + @Nullable final String account, + final Map<String, ExpandableBinaryDictionary> subDicts) { + mLocale = locale; + mAccount = account; + // The main dictionary can be asynchronously loaded. + setMainDict(mainDict); + for (final Map.Entry<String, ExpandableBinaryDictionary> 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; + } + return getSubDict(dictType); + } + + public ExpandableBinaryDictionary getSubDict(final String dictType) { + return mSubDictMap.get(dictType); + } + + public boolean hasDict(final String dictType, @Nullable final String account) { + if (Dictionary.TYPE_MAIN.equals(dictType)) { + return mMainDict != null; + } + if (Dictionary.TYPE_USER_HISTORY.equals(dictType) && + !TextUtils.equals(account, mAccount)) { + // If the dictionary type is user history, & if the account doesn't match, + // return immediately. If the account matches, continue looking it up in the + // sub dictionary map. + return false; + } + 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 DictionaryFacilitatorImpl() { + } + + @Override + public void onStartInput() { + } + + @Override + public void onFinishInput(Context context) { + } + + @Override + public boolean isActive() { + return mDictionaryGroup.mLocale != null; + } + + @Override + public Locale getLocale() { + return mDictionaryGroup.mLocale; + } + + @Override + public boolean usesContacts() { + return mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTACTS) != null; + } + + @Override + public String getAccount() { + return null; + } + + @Nullable + private static ExpandableBinaryDictionary getSubDict(final String dictType, + final Context context, final Locale locale, final File dictFile, + final String dictNamePrefix, @Nullable final String account) { + final Class<? extends ExpandableBinaryDictionary> 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, account }); + return (ExpandableBinaryDictionary) dict; + } catch (final NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException e) { + Log.e(TAG, "Cannot create dictionary: " + dictType, e); + return null; + } + } + + @Nullable + static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup, + final Locale locale) { + return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null; + } + + @Override + public void 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) { + final HashMap<Locale, ArrayList<String>> existingDictionariesToCleanup = new HashMap<>(); + // TODO: Make subDictTypesToUse configurable by resource or a static final list. + final HashSet<String> subDictTypesToUse = new HashSet<>(); + subDictTypesToUse.add(Dictionary.TYPE_USER); + + // Do not use contacts dictionary if we do not have permissions to read contacts. + final boolean contactsPermissionGranted = PermissionsUtil.checkAllPermissionsGranted( + context, Manifest.permission.READ_CONTACTS); + if (useContactsDict && contactsPermissionGranted) { + subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); + } + if (usePersonalizedDicts) { + subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); + } + + // Gather all dictionaries. We'll remove them from the list to clean up later. + final ArrayList<String> dictTypeForLocale = new ArrayList<>(); + existingDictionariesToCleanup.put(newLocale, dictTypeForLocale); + final DictionaryGroup currentDictionaryGroupForLocale = + findDictionaryGroupWithLocale(mDictionaryGroup, newLocale); + if (currentDictionaryGroupForLocale != null) { + for (final String dictType : DYNAMIC_DICTIONARY_TYPES) { + if (currentDictionaryGroupForLocale.hasDict(dictType, account)) { + dictTypeForLocale.add(dictType); + } + } + if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { + dictTypeForLocale.add(Dictionary.TYPE_MAIN); + } + } + + final DictionaryGroup dictionaryGroupForLocale = + findDictionaryGroupWithLocale(mDictionaryGroup, newLocale); + final ArrayList<String> dictTypesToCleanupForLocale = + existingDictionariesToCleanup.get(newLocale); + final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); + + final Dictionary mainDict; + if (forceReloadMainDictionary || noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { + mainDict = null; + } else { + mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); + dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN); + } + + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + for (final String subDictType : subDictTypesToUse) { + final ExpandableBinaryDictionary subDict; + if (noExistingDictsForThisLocale + || !dictionaryGroupForLocale.hasDict(subDictType, account)) { + // Create a new dictionary. + subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, + dictNamePrefix, account); + } else { + // Reuse the existing dictionary, and don't close it at the end + subDict = dictionaryGroupForLocale.getSubDict(subDictType); + dictTypesToCleanupForLocale.remove(subDictType); + } + subDicts.put(subDictType, subDict); + } + DictionaryGroup newDictionaryGroup = + new DictionaryGroup(newLocale, mainDict, account, subDicts); + + // Replace Dictionaries. + final DictionaryGroup oldDictionaryGroup; + synchronized (mLock) { + oldDictionaryGroup = mDictionaryGroup; + mDictionaryGroup = newDictionaryGroup; + if (hasAtLeastOneUninitializedMainDictionary()) { + asyncReloadUninitializedMainDictionaries(context, newLocale, listener); + } + } + if (listener != null) { + listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); + } + + // Clean up old dictionaries. + for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) { + final ArrayList<String> dictTypesToCleanUp = + existingDictionariesToCleanup.get(localeToCleanUp); + final DictionaryGroup dictionarySetToCleanup = + findDictionaryGroupWithLocale(oldDictionaryGroup, localeToCleanUp); + for (final String dictType : dictTypesToCleanUp) { + dictionarySetToCleanup.closeDict(dictType); + } + } + + if (mValidSpellingWordWriteCache != null) { + mValidSpellingWordWriteCache.evictAll(); + } + } + + private void asyncReloadUninitializedMainDictionaries(final Context context, + final Locale locale, final DictionaryInitializationListener listener) { + final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); + mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary; + ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() { + @Override + public void run() { + doReloadUninitializedMainDictionaries( + context, locale, listener, latchForWaitingLoadingMainDictionary); + } + }); + } + + void doReloadUninitializedMainDictionaries(final Context context, final Locale locale, + final DictionaryInitializationListener listener, + final CountDownLatch latchForWaitingLoadingMainDictionary) { + final DictionaryGroup dictionaryGroup = + findDictionaryGroupWithLocale(mDictionaryGroup, 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"); + return; + } + 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 locale, + final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, + final Map<String, Map<String, String>> additionalDictAttributes, + @Nullable final String account) { + Dictionary mainDictionary = null; + final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); + + 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 */, account); + 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); + } + } + mDictionaryGroup = new DictionaryGroup(locale, mainDictionary, account, subDicts); + } + + public void closeDictionaries() { + final DictionaryGroup dictionaryGroupToClose; + synchronized (mLock) { + dictionaryGroupToClose = mDictionaryGroup; + mDictionaryGroup = new DictionaryGroup(); + } + for (final String dictType : ALL_DICTIONARY_TYPES) { + dictionaryGroupToClose.closeDict(dictType); + } + } + + @UsedForTesting + public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { + return mDictionaryGroup.getSubDict(dictName); + } + + // The main dictionaries are loaded asynchronously. Don't cache the return value + // of these methods. + public boolean hasAtLeastOneInitializedMainDictionary() { + final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); + if (mainDict != null && mainDict.isInitialized()) { + return true; + } + return false; + } + + public boolean hasAtLeastOneUninitializedMainDictionary() { + final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); + if (mainDict == null || !mainDict.isInitialized()) { + return true; + } + return false; + } + + 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); + for (final ExpandableBinaryDictionary dict : mDictionaryGroup.mSubDictMap.values()) { + dict.waitAllTasksForTests(); + } + } + + public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, + @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, + final boolean blockPotentiallyOffensive) { + // Update the spelling cache before learning. Words that are not yet added to user history + // and appear in no other language model are not considered valid. + putWordIntoValidSpellingWordCache("addToUserHistory", suggestion); + + 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(mDictionaryGroup, ngramContextForCurrentWord, currentWord, + wasCurrentWordAutoCapitalized, (int) timeStampInSeconds, + blockPotentiallyOffensive); + ngramContextForCurrentWord = + ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); + } + } + + private void putWordIntoValidSpellingWordCache( + @Nonnull final String caller, + @Nonnull final String originalWord) { + if (mValidSpellingWordWriteCache == null) { + return; + } + + final String lowerCaseWord = originalWord.toLowerCase(getLocale()); + final boolean lowerCaseValid = isValidSpellingWord(lowerCaseWord); + mValidSpellingWordWriteCache.put(lowerCaseWord, lowerCaseValid); + + final String capitalWord = + StringUtils.capitalizeFirstAndDowncaseRest(originalWord, getLocale()); + final boolean capitalValid; + if (lowerCaseValid) { + // The lower case form of the word is valid, so the upper case must be valid. + capitalValid = true; + } else { + capitalValid = isValidSpellingWord(capitalWord); + } + mValidSpellingWordWriteCache.put(capitalWord, capitalValid); + } + + 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 || !isForLocale(userHistoryDictionary.mLocale)) { + return; + } + final int maxFreq = getFrequency(word); + if (maxFreq == 0 && blockPotentiallyOffensive) { + return; + } + final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); + final String secondWord; + if (wasAutoCapitalized) { + if (isValidSuggestionWord(word) && !isValidSuggestionWord(lowerCasedWord)) { + // 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, + null /* account */) ? + 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); + } + + private void removeWord(final String dictName, final String word) { + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); + if (dictionary != null) { + dictionary.removeUnigramEntryDynamically(word); + } + } + + @Override + public void unlearnFromUserHistory(final String word, + @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, + final int eventType) { + // TODO: Decide whether or not to remove the word on EVENT_BACKSPACE. + if (eventType != Constants.EVENT_BACKSPACE) { + removeWord(Dictionary.TYPE_USER_HISTORY, word); + } + + // Update the spelling cache after unlearning. Words that are removed from user history + // and appear in no other language model are not considered valid. + putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.toLowerCase()); + } + + // TODO: Revise the way to fusion suggestion results. + @Override + @Nonnull public SuggestionResults getSuggestionResults(ComposedData composedData, + NgramContext ngramContext, @Nonnull final Keyboard keyboard, + SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId, + int inputStyle) { + long proximityInfoHandle = keyboard.getProximityInfo().getNativeProximityInfo(); + final SuggestionResults suggestionResults = new SuggestionResults( + SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext(), + false /* firstSuggestionExceedsConfidenceThreshold */); + final float[] weightOfLangModelVsSpatialModel = + new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL }; + for (final String dictType : ALL_DICTIONARY_TYPES) { + final Dictionary dictionary = mDictionaryGroup.getDict(dictType); + if (null == dictionary) continue; + final float weightForLocale = composedData.mIsBatchMode + ? mDictionaryGroup.mWeightForGesturingInLocale + : mDictionaryGroup.mWeightForTypingInLocale; + final ArrayList<SuggestedWordInfo> dictionarySuggestions = + dictionary.getSuggestions(composedData, ngramContext, + proximityInfoHandle, settingsValuesForSuggestion, sessionId, + weightForLocale, weightOfLangModelVsSpatialModel); + if (null == dictionarySuggestions) continue; + suggestionResults.addAll(dictionarySuggestions); + if (null != suggestionResults.mRawSuggestions) { + suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); + } + } + return suggestionResults; + } + + public boolean isValidSpellingWord(final String word) { + if (mValidSpellingWordReadCache != null) { + final Boolean cachedValue = mValidSpellingWordReadCache.get(word); + if (cachedValue != null) { + return cachedValue; + } + } + + return isValidWord(word, ALL_DICTIONARY_TYPES); + } + + public boolean isValidSuggestionWord(final String word) { + return isValidWord(word, ALL_DICTIONARY_TYPES); + } + + private boolean isValidWord(final String word, final String[] dictionariesToCheck) { + if (TextUtils.isEmpty(word)) { + return false; + } + if (mDictionaryGroup.mLocale == null) { + return false; + } + for (final String dictType : dictionariesToCheck) { + final Dictionary dictionary = mDictionaryGroup.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)) { + return true; + } + } + return false; + } + + private int getFrequency(final String word) { + if (TextUtils.isEmpty(word)) { + return Dictionary.NOT_A_PROBABILITY; + } + int maxFreq = Dictionary.NOT_A_PROBABILITY; + for (final String dictType : ALL_DICTIONARY_TYPES) { + final Dictionary dictionary = mDictionaryGroup.getDict(dictType); + if (dictionary == null) continue; + final int tempFreq = dictionary.getFrequency(word); + if (tempFreq >= maxFreq) { + maxFreq = tempFreq; + } + } + return maxFreq; + } + + private boolean clearSubDictionary(final String dictName) { + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); + if (dictionary == null) { + return false; + } + dictionary.clear(); + return true; + } + + @Override + public boolean clearUserHistoryDictionary(final Context context) { + return clearSubDictionary(Dictionary.TYPE_USER_HISTORY); + } + + @Override + public void dumpDictionaryForDebug(final String dictName) { + final ExpandableBinaryDictionary dictToDump = mDictionaryGroup.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(); + } + + @Override + @Nonnull public List<DictionaryStats> getDictionaryStats(final Context context) { + final ArrayList<DictionaryStats> statsOfEnabledSubDicts = new ArrayList<>(); + for (final String dictType : DYNAMIC_DICTIONARY_TYPES) { + final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictType); + if (dictionary == null) continue; + statsOfEnabledSubDicts.add(dictionary.getDictionaryStats()); + } + return statsOfEnabledSubDicts; + } + + @Override + public String dump(final Context context) { + return ""; + } +} |