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