aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/org/kelar/inputmethod/latin/DictionaryFacilitatorImpl.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/DictionaryFacilitatorImpl.java')
-rw-r--r--java/src/org/kelar/inputmethod/latin/DictionaryFacilitatorImpl.java736
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 "";
+ }
+}