1/* 27 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin; 18 19import android.Manifest; 20import android.content.Context; 21import android.text.TextUtils; 22import android.util.Log; 23import android.util.LruCache; 24 25import com.android.inputmethod.annotations.UsedForTesting; 26import com.android.inputmethod.keyboard.Keyboard; 27import com.android.inputmethod.latin.NgramContext.WordInfo; 28import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 29import com.android.inputmethod.latin.common.ComposedData; 30import com.android.inputmethod.latin.common.Constants; 31import com.android.inputmethod.latin.common.StringUtils; 32import com.android.inputmethod.latin.permissions.PermissionsUtil; 33import com.android.inputmethod.latin.personalization.UserHistoryDictionary; 34import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; 35import com.android.inputmethod.latin.utils.ExecutorUtils; 36import com.android.inputmethod.latin.utils.SuggestionResults; 37 38import java.io.File; 39import java.lang.reflect.InvocationTargetException; 40import java.lang.reflect.Method; 41import java.util.ArrayList; 42import java.util.Collections; 43import java.util.HashMap; 44import java.util.HashSet; 45import java.util.List; 46import java.util.Locale; 47import java.util.Map; 48import java.util.concurrent.ConcurrentHashMap; 49import java.util.concurrent.CountDownLatch; 50import java.util.concurrent.TimeUnit; 51 52import javax.annotation.Nonnull; 53import javax.annotation.Nullable; 54 55/** 56 * Facilitates interaction with different kinds of dictionaries. Provides APIs 57 * to instantiate and select the correct dictionaries (based on language or account), 58 * update entries and fetch suggestions. 59 * 60 * Currently AndroidSpellCheckerService and LatinIME both use DictionaryFacilitator as 61 * a client for interacting with dictionaries. 62 */ 63public class DictionaryFacilitatorImpl implements DictionaryFacilitator { 64 // TODO: Consolidate dictionaries in native code. 65 public static final String TAG = DictionaryFacilitatorImpl.class.getSimpleName(); 66 67 // HACK: This threshold is being used when adding a capitalized entry in the User History 68 // dictionary. 69 private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; 70 71 private DictionaryGroup mDictionaryGroup = new DictionaryGroup(); 72 private volatile CountDownLatch mLatchForWaitingLoadingMainDictionaries = new CountDownLatch(0); 73 // To synchronize assigning mDictionaryGroup to ensure closing dictionaries. 74 private final Object mLock = new Object(); 75 76 public static final Map<String, Class<? extends ExpandableBinaryDictionary>> 77 DICT_TYPE_TO_CLASS = new HashMap<>(); 78 79 static { 80 DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class); 81 DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class); 82 DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class); 83 } 84 85 private static final String DICT_FACTORY_METHOD_NAME = "getDictionary"; 86 private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = 87 new Class[] { Context.class, Locale.class, File.class, String.class, String.class }; 88 89 private LruCache<String, Boolean> mValidSpellingWordReadCache; 90 private LruCache<String, Boolean> mValidSpellingWordWriteCache; 91 92 @Override 93 public void setValidSpellingWordReadCache(final LruCache<String, Boolean> cache) { 94 mValidSpellingWordReadCache = cache; 95 } 96 97 @Override 98 public void setValidSpellingWordWriteCache(final LruCache<String, Boolean> cache) { 99 mValidSpellingWordWriteCache = cache; 100 } 101 102 @Override 103 public boolean isForLocale(final Locale locale) { 104 return locale != null && locale.equals(mDictionaryGroup.mLocale); 105 } 106 107 /** 108 * Returns whether this facilitator is exactly for this account. 109 * 110 * @param account the account to test against. 111 */ 112 public boolean isForAccount(@Nullable final String account) { 113 return TextUtils.equals(mDictionaryGroup.mAccount, account); 114 } 115 116 /** 117 * A group of dictionaries that work together for a single language. 118 */ 119 private static class DictionaryGroup { 120 // TODO: Add null analysis annotations. 121 // TODO: Run evaluation to determine a reasonable value for these constants. The current 122 // values are ad-hoc and chosen without any particular care or methodology. 123 public static final float WEIGHT_FOR_MOST_PROBABLE_LANGUAGE = 1.0f; 124 public static final float WEIGHT_FOR_GESTURING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.95f; 125 public static final float WEIGHT_FOR_TYPING_IN_NOT_MOST_PROBABLE_LANGUAGE = 0.6f; 126 127 /** 128 * The locale associated with the dictionary group. 129 */ 130 @Nullable public final Locale mLocale; 131 132 /** 133 * The user account associated with the dictionary group. 134 */ 135 @Nullable public final String mAccount; 136 137 @Nullable private Dictionary mMainDict; 138 // Confidence that the most probable language is actually the language the user is 139 // typing in. For now, this is simply the number of times a word from this language 140 // has been committed in a row. 141 private int mConfidence = 0; 142 143 public float mWeightForTypingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; 144 public float mWeightForGesturingInLocale = WEIGHT_FOR_MOST_PROBABLE_LANGUAGE; 145 public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = 146 new ConcurrentHashMap<>(); 147 148 public DictionaryGroup() { 149 this(null /* locale */, null /* mainDict */, null /* account */, 150 Collections.<String, ExpandableBinaryDictionary>emptyMap() /* subDicts */); 151 } 152 153 public DictionaryGroup(@Nullable final Locale locale, 154 @Nullable final Dictionary mainDict, 155 @Nullable final String account, 156 final Map<String, ExpandableBinaryDictionary> subDicts) { 157 mLocale = locale; 158 mAccount = account; 159 // The main dictionary can be asynchronously loaded. 160 setMainDict(mainDict); 161 for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { 162 setSubDict(entry.getKey(), entry.getValue()); 163 } 164 } 165 166 private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) { 167 if (dict != null) { 168 mSubDictMap.put(dictType, dict); 169 } 170 } 171 172 public void setMainDict(final Dictionary mainDict) { 173 // Close old dictionary if exists. Main dictionary can be assigned multiple times. 174 final Dictionary oldDict = mMainDict; 175 mMainDict = mainDict; 176 if (oldDict != null && mainDict != oldDict) { 177 oldDict.close(); 178 } 179 } 180 181 public Dictionary getDict(final String dictType) { 182 if (Dictionary.TYPE_MAIN.equals(dictType)) { 183 return mMainDict; 184 } 185 return getSubDict(dictType); 186 } 187 188 public ExpandableBinaryDictionary getSubDict(final String dictType) { 189 return mSubDictMap.get(dictType); 190 } 191 192 public boolean hasDict(final String dictType, @Nullable final String account) { 193 if (Dictionary.TYPE_MAIN.equals(dictType)) { 194 return mMainDict != null; 195 } 196 if (Dictionary.TYPE_USER_HISTORY.equals(dictType) && 197 !TextUtils.equals(account, mAccount)) { 198 // If the dictionary type is user history, & if the account doesn't match, 199 // return immediately. If the account matches, continue looking it up in the 200 // sub dictionary map. 201 return false; 202 } 203 return mSubDictMap.containsKey(dictType); 204 } 205 206 public void closeDict(final String dictType) { 207 final Dictionary dict; 208 if (Dictionary.TYPE_MAIN.equals(dictType)) { 209 dict = mMainDict; 210 } else { 211 dict = mSubDictMap.remove(dictType); 212 } 213 if (dict != null) { 214 dict.close(); 215 } 216 } 217 } 218 219 public DictionaryFacilitatorImpl() { 220 } 221 222 @Override 223 public void onStartInput() { 224 } 225 226 @Override 227 public void onFinishInput(Context context) { 228 } 229 230 @Override 231 public boolean isActive() { 232 return mDictionaryGroup.mLocale != null; 233 } 234 235 @Override 236 public Locale getLocale() { 237 return mDictionaryGroup.mLocale; 238 } 239 240 @Override 241 public boolean usesContacts() { 242 return mDictionaryGroup.getSubDict(Dictionary.TYPE_CONTACTS) != null; 243 } 244 245 @Override 246 public String getAccount() { 247 return null; 248 } 249 250 @Nullable 251 private static ExpandableBinaryDictionary getSubDict(final String dictType, 252 final Context context, final Locale locale, final File dictFile, 253 final String dictNamePrefix, @Nullable final String account) { 254 final Class<? extends ExpandableBinaryDictionary> dictClass = 255 DICT_TYPE_TO_CLASS.get(dictType); 256 if (dictClass == null) { 257 return null; 258 } 259 try { 260 final Method factoryMethod = dictClass.getMethod(DICT_FACTORY_METHOD_NAME, 261 DICT_FACTORY_METHOD_ARG_TYPES); 262 final Object dict = factoryMethod.invoke(null /* obj */, 263 new Object[] { context, locale, dictFile, dictNamePrefix, account }); 264 return (ExpandableBinaryDictionary) dict; 265 } catch (final NoSuchMethodException | SecurityException | IllegalAccessException 266 | IllegalArgumentException | InvocationTargetException e) { 267 Log.e(TAG, "Cannot create dictionary: " + dictType, e); 268 return null; 269 } 270 } 271 272 @Nullable 273 static DictionaryGroup findDictionaryGroupWithLocale(final DictionaryGroup dictionaryGroup, 274 final Locale locale) { 275 return locale.equals(dictionaryGroup.mLocale) ? dictionaryGroup : null; 276 } 277 278 @Override 279 public void resetDictionaries( 280 final Context context, 281 final Locale newLocale, 282 final boolean useContactsDict, 283 final boolean usePersonalizedDicts, 284 final boolean forceReloadMainDictionary, 285 @Nullable final String account, 286 final String dictNamePrefix, 287 @Nullable final DictionaryInitializationListener listener) { 288 final HashMap<Locale, ArrayList<String>> existingDictionariesToCleanup = new HashMap<>(); 289 // TODO: Make subDictTypesToUse configurable by resource or a static final list. 290 final HashSet<String> subDictTypesToUse = new HashSet<>(); 291 subDictTypesToUse.add(Dictionary.TYPE_USER); 292 293 // Do not use contacts dictionary if we do not have permissions to read contacts. 294 final boolean contactsPermissionGranted = PermissionsUtil.checkAllPermissionsGranted( 295 context, Manifest.permission.READ_CONTACTS); 296 if (useContactsDict && contactsPermissionGranted) { 297 subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); 298 } 299 if (usePersonalizedDicts) { 300 subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); 301 } 302 303 // Gather all dictionaries. We'll remove them from the list to clean up later. 304 final ArrayList<String> dictTypeForLocale = new ArrayList<>(); 305 existingDictionariesToCleanup.put(newLocale, dictTypeForLocale); 306 final DictionaryGroup currentDictionaryGroupForLocale = 307 findDictionaryGroupWithLocale(mDictionaryGroup, newLocale); 308 if (currentDictionaryGroupForLocale != null) { 309 for (final String dictType : DYNAMIC_DICTIONARY_TYPES) { 310 if (currentDictionaryGroupForLocale.hasDict(dictType, account)) { 311 dictTypeForLocale.add(dictType); 312 } 313 } 314 if (currentDictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { 315 dictTypeForLocale.add(Dictionary.TYPE_MAIN); 316 } 317 } 318 319 final DictionaryGroup dictionaryGroupForLocale = 320 findDictionaryGroupWithLocale(mDictionaryGroup, newLocale); 321 final ArrayList<String> dictTypesToCleanupForLocale = 322 existingDictionariesToCleanup.get(newLocale); 323 final boolean noExistingDictsForThisLocale = (null == dictionaryGroupForLocale); 324 325 final Dictionary mainDict; 326 if (forceReloadMainDictionary || noExistingDictsForThisLocale 327 || !dictionaryGroupForLocale.hasDict(Dictionary.TYPE_MAIN, account)) { 328 mainDict = null; 329 } else { 330 mainDict = dictionaryGroupForLocale.getDict(Dictionary.TYPE_MAIN); 331 dictTypesToCleanupForLocale.remove(Dictionary.TYPE_MAIN); 332 } 333 334 final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); 335 for (final String subDictType : subDictTypesToUse) { 336 final ExpandableBinaryDictionary subDict; 337 if (noExistingDictsForThisLocale 338 || !dictionaryGroupForLocale.hasDict(subDictType, account)) { 339 // Create a new dictionary. 340 subDict = getSubDict(subDictType, context, newLocale, null /* dictFile */, 341 dictNamePrefix, account); 342 } else { 343 // Reuse the existing dictionary, and don't close it at the end 344 subDict = dictionaryGroupForLocale.getSubDict(subDictType); 345 dictTypesToCleanupForLocale.remove(subDictType); 346 } 347 subDicts.put(subDictType, subDict); 348 } 349 DictionaryGroup newDictionaryGroup = 350 new DictionaryGroup(newLocale, mainDict, account, subDicts); 351 352 // Replace Dictionaries. 353 final DictionaryGroup oldDictionaryGroup; 354 synchronized (mLock) { 355 oldDictionaryGroup = mDictionaryGroup; 356 mDictionaryGroup = newDictionaryGroup; 357 if (hasAtLeastOneUninitializedMainDictionary()) { 358 asyncReloadUninitializedMainDictionaries(context, newLocale, listener); 359 } 360 } 361 if (listener != null) { 362 listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); 363 } 364 365 // Clean up old dictionaries. 366 for (final Locale localeToCleanUp : existingDictionariesToCleanup.keySet()) { 367 final ArrayList<String> dictTypesToCleanUp = 368 existingDictionariesToCleanup.get(localeToCleanUp); 369 final DictionaryGroup dictionarySetToCleanup = 370 findDictionaryGroupWithLocale(oldDictionaryGroup, localeToCleanUp); 371 for (final String dictType : dictTypesToCleanUp) { 372 dictionarySetToCleanup.closeDict(dictType); 373 } 374 } 375 376 if (mValidSpellingWordWriteCache != null) { 377 mValidSpellingWordWriteCache.evictAll(); 378 } 379 } 380 381 private void asyncReloadUninitializedMainDictionaries(final Context context, 382 final Locale locale, final DictionaryInitializationListener listener) { 383 final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); 384 mLatchForWaitingLoadingMainDictionaries = latchForWaitingLoadingMainDictionary; 385 ExecutorUtils.getBackgroundExecutor(ExecutorUtils.KEYBOARD).execute(new Runnable() { 386 @Override 387 public void run() { 388 doReloadUninitializedMainDictionaries( 389 context, locale, listener, latchForWaitingLoadingMainDictionary); 390 } 391 }); 392 } 393 394 void doReloadUninitializedMainDictionaries(final Context context, final Locale locale, 395 final DictionaryInitializationListener listener, 396 final CountDownLatch latchForWaitingLoadingMainDictionary) { 397 final DictionaryGroup dictionaryGroup = 398 findDictionaryGroupWithLocale(mDictionaryGroup, locale); 399 if (null == dictionaryGroup) { 400 // This should never happen, but better safe than crashy 401 Log.w(TAG, "Expected a dictionary group for " + locale + " but none found"); 402 return; 403 } 404 final Dictionary mainDict = 405 DictionaryFactory.createMainDictionaryFromManager(context, locale); 406 synchronized (mLock) { 407 if (locale.equals(dictionaryGroup.mLocale)) { 408 dictionaryGroup.setMainDict(mainDict); 409 } else { 410 // Dictionary facilitator has been reset for another locale. 411 mainDict.close(); 412 } 413 } 414 if (listener != null) { 415 listener.onUpdateMainDictionaryAvailability(hasAtLeastOneInitializedMainDictionary()); 416 } 417 latchForWaitingLoadingMainDictionary.countDown(); 418 } 419 420 @UsedForTesting 421 public void resetDictionariesForTesting(final Context context, final Locale locale, 422 final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, 423 final Map<String, Map<String, String>> additionalDictAttributes, 424 @Nullable final String account) { 425 Dictionary mainDictionary = null; 426 final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); 427 428 for (final String dictType : dictionaryTypes) { 429 if (dictType.equals(Dictionary.TYPE_MAIN)) { 430 mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, 431 locale); 432 } else { 433 final File dictFile = dictionaryFiles.get(dictType); 434 final ExpandableBinaryDictionary dict = getSubDict( 435 dictType, context, locale, dictFile, "" /* dictNamePrefix */, account); 436 if (additionalDictAttributes.containsKey(dictType)) { 437 dict.clearAndFlushDictionaryWithAdditionalAttributes( 438 additionalDictAttributes.get(dictType)); 439 } 440 if (dict == null) { 441 throw new RuntimeException("Unknown dictionary type: " + dictType); 442 } 443 dict.reloadDictionaryIfRequired(); 444 dict.waitAllTasksForTests(); 445 subDicts.put(dictType, dict); 446 } 447 } 448 mDictionaryGroup = new DictionaryGroup(locale, mainDictionary, account, subDicts); 449 } 450 451 public void closeDictionaries() { 452 final DictionaryGroup dictionaryGroupToClose; 453 synchronized (mLock) { 454 dictionaryGroupToClose = mDictionaryGroup; 455 mDictionaryGroup = new DictionaryGroup(); 456 } 457 for (final String dictType : ALL_DICTIONARY_TYPES) { 458 dictionaryGroupToClose.closeDict(dictType); 459 } 460 } 461 462 @UsedForTesting 463 public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { 464 return mDictionaryGroup.getSubDict(dictName); 465 } 466 467 // The main dictionaries are loaded asynchronously. Don't cache the return value 468 // of these methods. 469 public boolean hasAtLeastOneInitializedMainDictionary() { 470 final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); 471 if (mainDict != null && mainDict.isInitialized()) { 472 return true; 473 } 474 return false; 475 } 476 477 public boolean hasAtLeastOneUninitializedMainDictionary() { 478 final Dictionary mainDict = mDictionaryGroup.getDict(Dictionary.TYPE_MAIN); 479 if (mainDict == null || !mainDict.isInitialized()) { 480 return true; 481 } 482 return false; 483 } 484 485 public void waitForLoadingMainDictionaries(final long timeout, final TimeUnit unit) 486 throws InterruptedException { 487 mLatchForWaitingLoadingMainDictionaries.await(timeout, unit); 488 } 489 490 @UsedForTesting 491 public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) 492 throws InterruptedException { 493 waitForLoadingMainDictionaries(timeout, unit); 494 for (final ExpandableBinaryDictionary dict : mDictionaryGroup.mSubDictMap.values()) { 495 dict.waitAllTasksForTests(); 496 } 497 } 498 499 public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, 500 @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, 501 final boolean blockPotentiallyOffensive) { 502 // Update the spelling cache before learning. Words that are not yet added to user history 503 // and appear in no other language model are not considered valid. 504 putWordIntoValidSpellingWordCache("addToUserHistory", suggestion); 505 506 final String[] words = suggestion.split(Constants.WORD_SEPARATOR); 507 NgramContext ngramContextForCurrentWord = ngramContext; 508 for (int i = 0; i < words.length; i++) { 509 final String currentWord = words[i]; 510 final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; 511 addWordToUserHistory(mDictionaryGroup, ngramContextForCurrentWord, currentWord, 512 wasCurrentWordAutoCapitalized, (int) timeStampInSeconds, 513 blockPotentiallyOffensive); 514 ngramContextForCurrentWord = 515 ngramContextForCurrentWord.getNextNgramContext(new WordInfo(currentWord)); 516 } 517 } 518 519 private void putWordIntoValidSpellingWordCache( 520 @Nonnull final String caller, 521 @Nonnull final String originalWord) { 522 if (mValidSpellingWordWriteCache == null) { 523 return; 524 } 525 526 final String lowerCaseWord = originalWord.toLowerCase(getLocale()); 527 final boolean lowerCaseValid = isValidSpellingWord(lowerCaseWord); 528 mValidSpellingWordWriteCache.put(lowerCaseWord, lowerCaseValid); 529 530 final String capitalWord = 531 StringUtils.capitalizeFirstAndDowncaseRest(originalWord, getLocale()); 532 final boolean capitalValid; 533 if (lowerCaseValid) { 534 // The lower case form of the word is valid, so the upper case must be valid. 535 capitalValid = true; 536 } else { 537 capitalValid = isValidSpellingWord(capitalWord); 538 } 539 mValidSpellingWordWriteCache.put(capitalWord, capitalValid); 540 } 541 542 private void addWordToUserHistory(final DictionaryGroup dictionaryGroup, 543 final NgramContext ngramContext, final String word, final boolean wasAutoCapitalized, 544 final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { 545 final ExpandableBinaryDictionary userHistoryDictionary = 546 dictionaryGroup.getSubDict(Dictionary.TYPE_USER_HISTORY); 547 if (userHistoryDictionary == null || !isForLocale(userHistoryDictionary.mLocale)) { 548 return; 549 } 550 final int maxFreq = getFrequency(word); 551 if (maxFreq == 0 && blockPotentiallyOffensive) { 552 return; 553 } 554 final String lowerCasedWord = word.toLowerCase(dictionaryGroup.mLocale); 555 final String secondWord; 556 if (wasAutoCapitalized) { 557 if (isValidSuggestionWord(word) && !isValidSuggestionWord(lowerCasedWord)) { 558 // If the word was auto-capitalized and exists only as a capitalized word in the 559 // dictionary, then we must not downcase it before registering it. For example, 560 // the name of the contacts in start-of-sentence position would come here with the 561 // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version 562 // of that contact's name which would end up popping in suggestions. 563 secondWord = word; 564 } else { 565 // If however the word is not in the dictionary, or exists as a lower-case word 566 // only, then we consider that was a lower-case word that had been auto-capitalized. 567 secondWord = lowerCasedWord; 568 } 569 } else { 570 // HACK: We'd like to avoid adding the capitalized form of common words to the User 571 // History dictionary in order to avoid suggesting them until the dictionary 572 // consolidation is done. 573 // TODO: Remove this hack when ready. 574 final int lowerCaseFreqInMainDict = dictionaryGroup.hasDict(Dictionary.TYPE_MAIN, 575 null /* account */) ? 576 dictionaryGroup.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : 577 Dictionary.NOT_A_PROBABILITY; 578 if (maxFreq < lowerCaseFreqInMainDict 579 && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { 580 // Use lower cased word as the word can be a distracter of the popular word. 581 secondWord = lowerCasedWord; 582 } else { 583 secondWord = word; 584 } 585 } 586 // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". 587 // We don't add words with 0-frequency (assuming they would be profanity etc.). 588 final boolean isValid = maxFreq > 0; 589 UserHistoryDictionary.addToDictionary(userHistoryDictionary, ngramContext, secondWord, 590 isValid, timeStampInSeconds); 591 } 592 593 private void removeWord(final String dictName, final String word) { 594 final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); 595 if (dictionary != null) { 596 dictionary.removeUnigramEntryDynamically(word); 597 } 598 } 599 600 @Override 601 public void unlearnFromUserHistory(final String word, 602 @Nonnull final NgramContext ngramContext, final long timeStampInSeconds, 603 final int eventType) { 604 // TODO: Decide whether or not to remove the word on EVENT_BACKSPACE. 605 if (eventType != Constants.EVENT_BACKSPACE) { 606 removeWord(Dictionary.TYPE_USER_HISTORY, word); 607 } 608 609 // Update the spelling cache after unlearning. Words that are removed from user history 610 // and appear in no other language model are not considered valid. 611 putWordIntoValidSpellingWordCache("unlearnFromUserHistory", word.toLowerCase()); 612 } 613 614 // TODO: Revise the way to fusion suggestion results. 615 @Override 616 @Nonnull public SuggestionResults getSuggestionResults(ComposedData composedData, 617 NgramContext ngramContext, @Nonnull final Keyboard keyboard, 618 SettingsValuesForSuggestion settingsValuesForSuggestion, int sessionId, 619 int inputStyle) { 620 long proximityInfoHandle = keyboard.getProximityInfo().getNativeProximityInfo(); 621 final SuggestionResults suggestionResults = new SuggestionResults( 622 SuggestedWords.MAX_SUGGESTIONS, ngramContext.isBeginningOfSentenceContext(), 623 false /* firstSuggestionExceedsConfidenceThreshold */); 624 final float[] weightOfLangModelVsSpatialModel = 625 new float[] { Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL }; 626 for (final String dictType : ALL_DICTIONARY_TYPES) { 627 final Dictionary dictionary = mDictionaryGroup.getDict(dictType); 628 if (null == dictionary) continue; 629 final float weightForLocale = composedData.mIsBatchMode 630 ? mDictionaryGroup.mWeightForGesturingInLocale 631 : mDictionaryGroup.mWeightForTypingInLocale; 632 final ArrayList<SuggestedWordInfo> dictionarySuggestions = 633 dictionary.getSuggestions(composedData, ngramContext, 634 proximityInfoHandle, settingsValuesForSuggestion, sessionId, 635 weightForLocale, weightOfLangModelVsSpatialModel); 636 if (null == dictionarySuggestions) continue; 637 suggestionResults.addAll(dictionarySuggestions); 638 if (null != suggestionResults.mRawSuggestions) { 639 suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); 640 } 641 } 642 return suggestionResults; 643 } 644 645 public boolean isValidSpellingWord(final String word) { 646 if (mValidSpellingWordReadCache != null) { 647 final Boolean cachedValue = mValidSpellingWordReadCache.get(word); 648 if (cachedValue != null) { 649 return cachedValue; 650 } 651 } 652 653 return isValidWord(word, ALL_DICTIONARY_TYPES); 654 } 655 656 public boolean isValidSuggestionWord(final String word) { 657 return isValidWord(word, ALL_DICTIONARY_TYPES); 658 } 659 660 private boolean isValidWord(final String word, final String[] dictionariesToCheck) { 661 if (TextUtils.isEmpty(word)) { 662 return false; 663 } 664 if (mDictionaryGroup.mLocale == null) { 665 return false; 666 } 667 for (final String dictType : dictionariesToCheck) { 668 final Dictionary dictionary = mDictionaryGroup.getDict(dictType); 669 // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and 670 // would be immutable once it's finished initializing, but concretely a null test is 671 // probably good enough for the time being. 672 if (null == dictionary) continue; 673 if (dictionary.isValidWord(word)) { 674 return true; 675 } 676 } 677 return false; 678 } 679 680 private int getFrequency(final String word) { 681 if (TextUtils.isEmpty(word)) { 682 return Dictionary.NOT_A_PROBABILITY; 683 } 684 int maxFreq = Dictionary.NOT_A_PROBABILITY; 685 for (final String dictType : ALL_DICTIONARY_TYPES) { 686 final Dictionary dictionary = mDictionaryGroup.getDict(dictType); 687 if (dictionary == null) continue; 688 final int tempFreq = dictionary.getFrequency(word); 689 if (tempFreq >= maxFreq) { 690 maxFreq = tempFreq; 691 } 692 } 693 return maxFreq; 694 } 695 696 private boolean clearSubDictionary(final String dictName) { 697 final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictName); 698 if (dictionary == null) { 699 return false; 700 } 701 dictionary.clear(); 702 return true; 703 } 704 705 @Override 706 public boolean clearUserHistoryDictionary(final Context context) { 707 return clearSubDictionary(Dictionary.TYPE_USER_HISTORY); 708 } 709 710 @Override 711 public void dumpDictionaryForDebug(final String dictName) { 712 final ExpandableBinaryDictionary dictToDump = mDictionaryGroup.getSubDict(dictName); 713 if (dictToDump == null) { 714 Log.e(TAG, "Cannot dump " + dictName + ". " 715 + "The dictionary is not being used for suggestion or cannot be dumped."); 716 return; 717 } 718 dictToDump.dumpAllWordsForDebug(); 719 } 720 721 @Override 722 @Nonnull public List<DictionaryStats> getDictionaryStats(final Context context) { 723 final ArrayList<DictionaryStats> statsOfEnabledSubDicts = new ArrayList<>(); 724 for (final String dictType : DYNAMIC_DICTIONARY_TYPES) { 725 final ExpandableBinaryDictionary dictionary = mDictionaryGroup.getSubDict(dictType); 726 if (dictionary == null) continue; 727 statsOfEnabledSubDicts.add(dictionary.getDictionaryStats()); 728 } 729 return statsOfEnabledSubDicts; 730 } 731 732 @Override 733 public String dump(final Context context) { 734 return ""; 735 } 736} 737