1 /* 2 * 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.content.Context; 20 import android.text.TextUtils; 21 import android.util.Log; 22 import android.view.inputmethod.InputMethodSubtype; 23 24 import com.android.inputmethod.annotations.UsedForTesting; 25 import com.android.inputmethod.keyboard.ProximityInfo; 26 import com.android.inputmethod.latin.PrevWordsInfo.WordInfo; 27 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 28 import com.android.inputmethod.latin.personalization.ContextualDictionary; 29 import com.android.inputmethod.latin.personalization.PersonalizationDataChunk; 30 import com.android.inputmethod.latin.personalization.PersonalizationDictionary; 31 import com.android.inputmethod.latin.personalization.UserHistoryDictionary; 32 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; 33 import com.android.inputmethod.latin.settings.SpacingAndPunctuations; 34 import com.android.inputmethod.latin.utils.DistracterFilter; 35 import com.android.inputmethod.latin.utils.DistracterFilterCheckingIsInDictionary; 36 import com.android.inputmethod.latin.utils.ExecutorUtils; 37 import com.android.inputmethod.latin.utils.LanguageModelParam; 38 import com.android.inputmethod.latin.utils.SuggestionResults; 39 40 import java.io.File; 41 import java.lang.reflect.InvocationTargetException; 42 import java.lang.reflect.Method; 43 import java.util.ArrayList; 44 import java.util.Arrays; 45 import java.util.HashMap; 46 import java.util.HashSet; 47 import java.util.List; 48 import java.util.Locale; 49 import java.util.Map; 50 import java.util.concurrent.ConcurrentHashMap; 51 import java.util.concurrent.CountDownLatch; 52 import java.util.concurrent.TimeUnit; 53 54 // TODO: Consolidate dictionaries in native code. 55 public class DictionaryFacilitator { 56 public static final String TAG = DictionaryFacilitator.class.getSimpleName(); 57 58 // HACK: This threshold is being used when adding a capitalized entry in the User History 59 // dictionary. 60 private static final int CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT = 140; 61 62 private Dictionaries mDictionaries = new Dictionaries(); 63 private boolean mIsUserDictEnabled = false; 64 private volatile CountDownLatch mLatchForWaitingLoadingMainDictionary = new CountDownLatch(0); 65 // To synchronize assigning mDictionaries to ensure closing dictionaries. 66 private final Object mLock = new Object(); 67 private final DistracterFilter mDistracterFilter; 68 69 private static final String[] DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS = 70 new String[] { 71 Dictionary.TYPE_MAIN, 72 Dictionary.TYPE_USER_HISTORY, 73 Dictionary.TYPE_PERSONALIZATION, 74 Dictionary.TYPE_USER, 75 Dictionary.TYPE_CONTACTS, 76 Dictionary.TYPE_CONTEXTUAL 77 }; 78 79 public static final Map<String, Class<? extends ExpandableBinaryDictionary>> 80 DICT_TYPE_TO_CLASS = new HashMap<>(); 81 82 static { DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class)83 DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER_HISTORY, UserHistoryDictionary.class); DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_PERSONALIZATION, PersonalizationDictionary.class)84 DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_PERSONALIZATION, PersonalizationDictionary.class); DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class)85 DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_USER, UserBinaryDictionary.class); DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class)86 DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTACTS, ContactsBinaryDictionary.class); DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTEXTUAL, ContextualDictionary.class)87 DICT_TYPE_TO_CLASS.put(Dictionary.TYPE_CONTEXTUAL, ContextualDictionary.class); 88 } 89 90 private static final String DICT_FACTORY_METHOD_NAME = "getDictionary"; 91 private static final Class<?>[] DICT_FACTORY_METHOD_ARG_TYPES = 92 new Class[] { Context.class, Locale.class, File.class, String.class }; 93 94 private static final String[] SUB_DICT_TYPES = 95 Arrays.copyOfRange(DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS, 1 /* start */, 96 DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS.length); 97 98 /** 99 * Class contains dictionaries for a locale. 100 */ 101 private static class Dictionaries { 102 public final Locale mLocale; 103 private Dictionary mMainDict; 104 public final ConcurrentHashMap<String, ExpandableBinaryDictionary> mSubDictMap = 105 new ConcurrentHashMap<>(); 106 Dictionaries()107 public Dictionaries() { 108 mLocale = null; 109 } 110 Dictionaries(final Locale locale, final Dictionary mainDict, final Map<String, ExpandableBinaryDictionary> subDicts)111 public Dictionaries(final Locale locale, final Dictionary mainDict, 112 final Map<String, ExpandableBinaryDictionary> subDicts) { 113 mLocale = locale; 114 // Main dictionary can be asynchronously loaded. 115 setMainDict(mainDict); 116 for (final Map.Entry<String, ExpandableBinaryDictionary> entry : subDicts.entrySet()) { 117 setSubDict(entry.getKey(), entry.getValue()); 118 } 119 } 120 setSubDict(final String dictType, final ExpandableBinaryDictionary dict)121 private void setSubDict(final String dictType, final ExpandableBinaryDictionary dict) { 122 if (dict != null) { 123 mSubDictMap.put(dictType, dict); 124 } 125 } 126 setMainDict(final Dictionary mainDict)127 public void setMainDict(final Dictionary mainDict) { 128 // Close old dictionary if exists. Main dictionary can be assigned multiple times. 129 final Dictionary oldDict = mMainDict; 130 mMainDict = mainDict; 131 if (oldDict != null && mainDict != oldDict) { 132 oldDict.close(); 133 } 134 } 135 getDict(final String dictType)136 public Dictionary getDict(final String dictType) { 137 if (Dictionary.TYPE_MAIN.equals(dictType)) { 138 return mMainDict; 139 } else { 140 return getSubDict(dictType); 141 } 142 } 143 getSubDict(final String dictType)144 public ExpandableBinaryDictionary getSubDict(final String dictType) { 145 return mSubDictMap.get(dictType); 146 } 147 hasDict(final String dictType)148 public boolean hasDict(final String dictType) { 149 if (Dictionary.TYPE_MAIN.equals(dictType)) { 150 return mMainDict != null; 151 } else { 152 return mSubDictMap.containsKey(dictType); 153 } 154 } 155 closeDict(final String dictType)156 public void closeDict(final String dictType) { 157 final Dictionary dict; 158 if (Dictionary.TYPE_MAIN.equals(dictType)) { 159 dict = mMainDict; 160 } else { 161 dict = mSubDictMap.remove(dictType); 162 } 163 if (dict != null) { 164 dict.close(); 165 } 166 } 167 } 168 169 public interface DictionaryInitializationListener { onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable)170 public void onUpdateMainDictionaryAvailability(boolean isMainDictionaryAvailable); 171 } 172 DictionaryFacilitator()173 public DictionaryFacilitator() { 174 mDistracterFilter = DistracterFilter.EMPTY_DISTRACTER_FILTER; 175 } 176 DictionaryFacilitator(final DistracterFilter distracterFilter)177 public DictionaryFacilitator(final DistracterFilter distracterFilter) { 178 mDistracterFilter = distracterFilter; 179 } 180 updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes)181 public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) { 182 mDistracterFilter.updateEnabledSubtypes(enabledSubtypes); 183 } 184 getLocale()185 public Locale getLocale() { 186 return mDictionaries.mLocale; 187 } 188 getSubDict(final String dictType, final Context context, final Locale locale, final File dictFile, final String dictNamePrefix)189 private static ExpandableBinaryDictionary getSubDict(final String dictType, 190 final Context context, final Locale locale, final File dictFile, 191 final String dictNamePrefix) { 192 final Class<? extends ExpandableBinaryDictionary> dictClass = 193 DICT_TYPE_TO_CLASS.get(dictType); 194 if (dictClass == null) { 195 return null; 196 } 197 try { 198 final Method factoryMethod = dictClass.getMethod(DICT_FACTORY_METHOD_NAME, 199 DICT_FACTORY_METHOD_ARG_TYPES); 200 final Object dict = factoryMethod.invoke(null /* obj */, 201 new Object[] { context, locale, dictFile, dictNamePrefix }); 202 return (ExpandableBinaryDictionary) dict; 203 } catch (final NoSuchMethodException | SecurityException | IllegalAccessException 204 | IllegalArgumentException | InvocationTargetException e) { 205 Log.e(TAG, "Cannot create dictionary: " + dictType, e); 206 return null; 207 } 208 } 209 resetDictionaries(final Context context, final Locale newLocale, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener)210 public void resetDictionaries(final Context context, final Locale newLocale, 211 final boolean useContactsDict, final boolean usePersonalizedDicts, 212 final boolean forceReloadMainDictionary, 213 final DictionaryInitializationListener listener) { 214 resetDictionariesWithDictNamePrefix(context, newLocale, useContactsDict, 215 usePersonalizedDicts, forceReloadMainDictionary, listener, "" /* dictNamePrefix */); 216 } 217 resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale, final boolean useContactsDict, final boolean usePersonalizedDicts, final boolean forceReloadMainDictionary, final DictionaryInitializationListener listener, final String dictNamePrefix)218 public void resetDictionariesWithDictNamePrefix(final Context context, final Locale newLocale, 219 final boolean useContactsDict, final boolean usePersonalizedDicts, 220 final boolean forceReloadMainDictionary, 221 final DictionaryInitializationListener listener, 222 final String dictNamePrefix) { 223 final boolean localeHasBeenChanged = !newLocale.equals(mDictionaries.mLocale); 224 // We always try to have the main dictionary. Other dictionaries can be unused. 225 final boolean reloadMainDictionary = localeHasBeenChanged || forceReloadMainDictionary; 226 // TODO: Make subDictTypesToUse configurable by resource or a static final list. 227 final HashSet<String> subDictTypesToUse = new HashSet<>(); 228 if (useContactsDict) { 229 subDictTypesToUse.add(Dictionary.TYPE_CONTACTS); 230 } 231 subDictTypesToUse.add(Dictionary.TYPE_USER); 232 if (usePersonalizedDicts) { 233 subDictTypesToUse.add(Dictionary.TYPE_USER_HISTORY); 234 subDictTypesToUse.add(Dictionary.TYPE_PERSONALIZATION); 235 subDictTypesToUse.add(Dictionary.TYPE_CONTEXTUAL); 236 } 237 238 final Dictionary newMainDict; 239 if (reloadMainDictionary) { 240 // The main dictionary will be asynchronously loaded. 241 newMainDict = null; 242 } else { 243 newMainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); 244 } 245 246 final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); 247 for (final String dictType : SUB_DICT_TYPES) { 248 if (!subDictTypesToUse.contains(dictType)) { 249 // This dictionary will not be used. 250 continue; 251 } 252 final ExpandableBinaryDictionary dict; 253 if (!localeHasBeenChanged && mDictionaries.hasDict(dictType)) { 254 // Continue to use current dictionary. 255 dict = mDictionaries.getSubDict(dictType); 256 } else { 257 // Start to use new dictionary. 258 dict = getSubDict(dictType, context, newLocale, null /* dictFile */, 259 dictNamePrefix); 260 } 261 subDicts.put(dictType, dict); 262 } 263 264 // Replace Dictionaries. 265 final Dictionaries newDictionaries = new Dictionaries(newLocale, newMainDict, subDicts); 266 final Dictionaries oldDictionaries; 267 synchronized (mLock) { 268 oldDictionaries = mDictionaries; 269 mDictionaries = newDictionaries; 270 mIsUserDictEnabled = UserBinaryDictionary.isEnabled(context); 271 if (reloadMainDictionary) { 272 asyncReloadMainDictionary(context, newLocale, listener); 273 } 274 } 275 if (listener != null) { 276 listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); 277 } 278 // Clean up old dictionaries. 279 if (reloadMainDictionary) { 280 oldDictionaries.closeDict(Dictionary.TYPE_MAIN); 281 } 282 for (final String dictType : SUB_DICT_TYPES) { 283 if (localeHasBeenChanged || !subDictTypesToUse.contains(dictType)) { 284 oldDictionaries.closeDict(dictType); 285 } 286 } 287 oldDictionaries.mSubDictMap.clear(); 288 } 289 asyncReloadMainDictionary(final Context context, final Locale locale, final DictionaryInitializationListener listener)290 private void asyncReloadMainDictionary(final Context context, final Locale locale, 291 final DictionaryInitializationListener listener) { 292 final CountDownLatch latchForWaitingLoadingMainDictionary = new CountDownLatch(1); 293 mLatchForWaitingLoadingMainDictionary = latchForWaitingLoadingMainDictionary; 294 ExecutorUtils.getExecutor("InitializeBinaryDictionary").execute(new Runnable() { 295 @Override 296 public void run() { 297 final Dictionary mainDict = 298 DictionaryFactory.createMainDictionaryFromManager(context, locale); 299 synchronized (mLock) { 300 if (locale.equals(mDictionaries.mLocale)) { 301 mDictionaries.setMainDict(mainDict); 302 } else { 303 // Dictionary facilitator has been reset for another locale. 304 mainDict.close(); 305 } 306 } 307 if (listener != null) { 308 listener.onUpdateMainDictionaryAvailability(hasInitializedMainDictionary()); 309 } 310 latchForWaitingLoadingMainDictionary.countDown(); 311 } 312 }); 313 } 314 315 @UsedForTesting resetDictionariesForTesting(final Context context, final Locale locale, final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, final Map<String, Map<String, String>> additionalDictAttributes)316 public void resetDictionariesForTesting(final Context context, final Locale locale, 317 final ArrayList<String> dictionaryTypes, final HashMap<String, File> dictionaryFiles, 318 final Map<String, Map<String, String>> additionalDictAttributes) { 319 Dictionary mainDictionary = null; 320 final Map<String, ExpandableBinaryDictionary> subDicts = new HashMap<>(); 321 322 for (final String dictType : dictionaryTypes) { 323 if (dictType.equals(Dictionary.TYPE_MAIN)) { 324 mainDictionary = DictionaryFactory.createMainDictionaryFromManager(context, locale); 325 } else { 326 final File dictFile = dictionaryFiles.get(dictType); 327 final ExpandableBinaryDictionary dict = getSubDict( 328 dictType, context, locale, dictFile, "" /* dictNamePrefix */); 329 if (additionalDictAttributes.containsKey(dictType)) { 330 dict.clearAndFlushDictionaryWithAdditionalAttributes( 331 additionalDictAttributes.get(dictType)); 332 } 333 if (dict == null) { 334 throw new RuntimeException("Unknown dictionary type: " + dictType); 335 } 336 dict.reloadDictionaryIfRequired(); 337 dict.waitAllTasksForTests(); 338 subDicts.put(dictType, dict); 339 } 340 } 341 mDictionaries = new Dictionaries(locale, mainDictionary, subDicts); 342 } 343 closeDictionaries()344 public void closeDictionaries() { 345 final Dictionaries dictionaries; 346 synchronized (mLock) { 347 dictionaries = mDictionaries; 348 mDictionaries = new Dictionaries(); 349 } 350 for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { 351 dictionaries.closeDict(dictType); 352 } 353 mDistracterFilter.close(); 354 } 355 356 @UsedForTesting getSubDictForTesting(final String dictName)357 public ExpandableBinaryDictionary getSubDictForTesting(final String dictName) { 358 return mDictionaries.getSubDict(dictName); 359 } 360 361 // The main dictionary could have been loaded asynchronously. Don't cache the return value 362 // of this method. hasInitializedMainDictionary()363 public boolean hasInitializedMainDictionary() { 364 final Dictionary mainDict = mDictionaries.getDict(Dictionary.TYPE_MAIN); 365 return mainDict != null && mainDict.isInitialized(); 366 } 367 hasPersonalizationDictionary()368 public boolean hasPersonalizationDictionary() { 369 return mDictionaries.hasDict(Dictionary.TYPE_PERSONALIZATION); 370 } 371 flushPersonalizationDictionary()372 public void flushPersonalizationDictionary() { 373 final ExpandableBinaryDictionary personalizationDict = 374 mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); 375 if (personalizationDict != null) { 376 personalizationDict.asyncFlushBinaryDictionary(); 377 } 378 } 379 waitForLoadingMainDictionary(final long timeout, final TimeUnit unit)380 public void waitForLoadingMainDictionary(final long timeout, final TimeUnit unit) 381 throws InterruptedException { 382 mLatchForWaitingLoadingMainDictionary.await(timeout, unit); 383 } 384 385 @UsedForTesting waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit)386 public void waitForLoadingDictionariesForTesting(final long timeout, final TimeUnit unit) 387 throws InterruptedException { 388 waitForLoadingMainDictionary(timeout, unit); 389 final Map<String, ExpandableBinaryDictionary> dictMap = mDictionaries.mSubDictMap; 390 for (final ExpandableBinaryDictionary dict : dictMap.values()) { 391 dict.waitAllTasksForTests(); 392 } 393 } 394 isUserDictionaryEnabled()395 public boolean isUserDictionaryEnabled() { 396 return mIsUserDictEnabled; 397 } 398 addWordToUserDictionary(final Context context, final String word)399 public void addWordToUserDictionary(final Context context, final String word) { 400 final Locale locale = getLocale(); 401 if (locale == null) { 402 return; 403 } 404 UserBinaryDictionary.addWordToUserDictionary(context, locale, word); 405 } 406 addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds, final boolean blockPotentiallyOffensive)407 public void addToUserHistory(final String suggestion, final boolean wasAutoCapitalized, 408 final PrevWordsInfo prevWordsInfo, final int timeStampInSeconds, 409 final boolean blockPotentiallyOffensive) { 410 final Dictionaries dictionaries = mDictionaries; 411 final String[] words = suggestion.split(Constants.WORD_SEPARATOR); 412 PrevWordsInfo prevWordsInfoForCurrentWord = prevWordsInfo; 413 for (int i = 0; i < words.length; i++) { 414 final String currentWord = words[i]; 415 final boolean wasCurrentWordAutoCapitalized = (i == 0) ? wasAutoCapitalized : false; 416 addWordToUserHistory(dictionaries, prevWordsInfoForCurrentWord, currentWord, 417 wasCurrentWordAutoCapitalized, timeStampInSeconds, blockPotentiallyOffensive); 418 prevWordsInfoForCurrentWord = 419 prevWordsInfoForCurrentWord.getNextPrevWordsInfo(new WordInfo(currentWord)); 420 } 421 } 422 addWordToUserHistory(final Dictionaries dictionaries, final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized, final int timeStampInSeconds, final boolean blockPotentiallyOffensive)423 private void addWordToUserHistory(final Dictionaries dictionaries, 424 final PrevWordsInfo prevWordsInfo, final String word, final boolean wasAutoCapitalized, 425 final int timeStampInSeconds, final boolean blockPotentiallyOffensive) { 426 final ExpandableBinaryDictionary userHistoryDictionary = 427 dictionaries.getSubDict(Dictionary.TYPE_USER_HISTORY); 428 if (userHistoryDictionary == null) { 429 return; 430 } 431 final int maxFreq = getFrequency(word); 432 if (maxFreq == 0 && blockPotentiallyOffensive) { 433 return; 434 } 435 final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); 436 final String secondWord; 437 if (wasAutoCapitalized) { 438 if (isValidWord(word, false /* ignoreCase */) 439 && !isValidWord(lowerCasedWord, false /* ignoreCase */)) { 440 // If the word was auto-capitalized and exists only as a capitalized word in the 441 // dictionary, then we must not downcase it before registering it. For example, 442 // the name of the contacts in start-of-sentence position would come here with the 443 // wasAutoCapitalized flag: if we downcase it, we'd register a lower-case version 444 // of that contact's name which would end up popping in suggestions. 445 secondWord = word; 446 } else { 447 // If however the word is not in the dictionary, or exists as a lower-case word 448 // only, then we consider that was a lower-case word that had been auto-capitalized. 449 secondWord = lowerCasedWord; 450 } 451 } else { 452 // HACK: We'd like to avoid adding the capitalized form of common words to the User 453 // History dictionary in order to avoid suggesting them until the dictionary 454 // consolidation is done. 455 // TODO: Remove this hack when ready. 456 final int lowerCaseFreqInMainDict = dictionaries.hasDict(Dictionary.TYPE_MAIN) ? 457 dictionaries.getDict(Dictionary.TYPE_MAIN).getFrequency(lowerCasedWord) : 458 Dictionary.NOT_A_PROBABILITY; 459 if (maxFreq < lowerCaseFreqInMainDict 460 && lowerCaseFreqInMainDict >= CAPITALIZED_FORM_MAX_PROBABILITY_FOR_INSERT) { 461 // Use lower cased word as the word can be a distracter of the popular word. 462 secondWord = lowerCasedWord; 463 } else { 464 secondWord = word; 465 } 466 } 467 // We demote unrecognized words (frequency < 0, below) by specifying them as "invalid". 468 // We don't add words with 0-frequency (assuming they would be profanity etc.). 469 final boolean isValid = maxFreq > 0; 470 UserHistoryDictionary.addToDictionary(userHistoryDictionary, prevWordsInfo, secondWord, 471 isValid, timeStampInSeconds, 472 new DistracterFilterCheckingIsInDictionary( 473 mDistracterFilter, userHistoryDictionary)); 474 } 475 removeWord(final String dictName, final String word)476 private void removeWord(final String dictName, final String word) { 477 final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); 478 if (dictionary != null) { 479 dictionary.removeUnigramEntryDynamically(word); 480 } 481 } 482 removeWordFromPersonalizedDicts(final String word)483 public void removeWordFromPersonalizedDicts(final String word) { 484 removeWord(Dictionary.TYPE_USER_HISTORY, word); 485 removeWord(Dictionary.TYPE_PERSONALIZATION, word); 486 removeWord(Dictionary.TYPE_CONTEXTUAL, word); 487 } 488 489 // TODO: Revise the way to fusion suggestion results. getSuggestionResults(final WordComposer composer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId)490 public SuggestionResults getSuggestionResults(final WordComposer composer, 491 final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, 492 final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) { 493 final Dictionaries dictionaries = mDictionaries; 494 final SuggestionResults suggestionResults = new SuggestionResults( 495 dictionaries.mLocale, SuggestedWords.MAX_SUGGESTIONS, 496 prevWordsInfo.mPrevWordsInfo[0].mIsBeginningOfSentence); 497 final float[] languageWeight = new float[] { Dictionary.NOT_A_LANGUAGE_WEIGHT }; 498 for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { 499 final Dictionary dictionary = dictionaries.getDict(dictType); 500 if (null == dictionary) continue; 501 final ArrayList<SuggestedWordInfo> dictionarySuggestions = 502 dictionary.getSuggestions(composer, prevWordsInfo, proximityInfo, 503 settingsValuesForSuggestion, sessionId, languageWeight); 504 if (null == dictionarySuggestions) continue; 505 suggestionResults.addAll(dictionarySuggestions); 506 if (null != suggestionResults.mRawSuggestions) { 507 suggestionResults.mRawSuggestions.addAll(dictionarySuggestions); 508 } 509 } 510 return suggestionResults; 511 } 512 isValidWord(final String word, final boolean ignoreCase)513 public boolean isValidWord(final String word, final boolean ignoreCase) { 514 if (TextUtils.isEmpty(word)) { 515 return false; 516 } 517 final Dictionaries dictionaries = mDictionaries; 518 if (dictionaries.mLocale == null) { 519 return false; 520 } 521 final String lowerCasedWord = word.toLowerCase(dictionaries.mLocale); 522 for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { 523 final Dictionary dictionary = dictionaries.getDict(dictType); 524 // Ideally the passed map would come out of a {@link java.util.concurrent.Future} and 525 // would be immutable once it's finished initializing, but concretely a null test is 526 // probably good enough for the time being. 527 if (null == dictionary) continue; 528 if (dictionary.isValidWord(word) 529 || (ignoreCase && dictionary.isValidWord(lowerCasedWord))) { 530 return true; 531 } 532 } 533 return false; 534 } 535 getFrequencyInternal(final String word, final boolean isGettingMaxFrequencyOfExactMatches)536 private int getFrequencyInternal(final String word, 537 final boolean isGettingMaxFrequencyOfExactMatches) { 538 if (TextUtils.isEmpty(word)) { 539 return Dictionary.NOT_A_PROBABILITY; 540 } 541 int maxFreq = Dictionary.NOT_A_PROBABILITY; 542 final Dictionaries dictionaries = mDictionaries; 543 for (final String dictType : DICT_TYPES_ORDERED_TO_GET_SUGGESTIONS) { 544 final Dictionary dictionary = dictionaries.getDict(dictType); 545 if (dictionary == null) continue; 546 final int tempFreq; 547 if (isGettingMaxFrequencyOfExactMatches) { 548 tempFreq = dictionary.getMaxFrequencyOfExactMatches(word); 549 } else { 550 tempFreq = dictionary.getFrequency(word); 551 } 552 if (tempFreq >= maxFreq) { 553 maxFreq = tempFreq; 554 } 555 } 556 return maxFreq; 557 } 558 getFrequency(final String word)559 public int getFrequency(final String word) { 560 return getFrequencyInternal(word, false /* isGettingMaxFrequencyOfExactMatches */); 561 } 562 getMaxFrequencyOfExactMatches(final String word)563 public int getMaxFrequencyOfExactMatches(final String word) { 564 return getFrequencyInternal(word, true /* isGettingMaxFrequencyOfExactMatches */); 565 } 566 clearSubDictionary(final String dictName)567 private void clearSubDictionary(final String dictName) { 568 final ExpandableBinaryDictionary dictionary = mDictionaries.getSubDict(dictName); 569 if (dictionary != null) { 570 dictionary.clear(); 571 } 572 } 573 clearUserHistoryDictionary()574 public void clearUserHistoryDictionary() { 575 clearSubDictionary(Dictionary.TYPE_USER_HISTORY); 576 } 577 578 // This method gets called only when the IME receives a notification to remove the 579 // personalization dictionary. clearPersonalizationDictionary()580 public void clearPersonalizationDictionary() { 581 clearSubDictionary(Dictionary.TYPE_PERSONALIZATION); 582 } 583 clearContextualDictionary()584 public void clearContextualDictionary() { 585 clearSubDictionary(Dictionary.TYPE_CONTEXTUAL); 586 } 587 addEntriesToPersonalizationDictionary( final PersonalizationDataChunk personalizationDataChunk, final SpacingAndPunctuations spacingAndPunctuations, final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback)588 public void addEntriesToPersonalizationDictionary( 589 final PersonalizationDataChunk personalizationDataChunk, 590 final SpacingAndPunctuations spacingAndPunctuations, 591 final ExpandableBinaryDictionary.AddMultipleDictionaryEntriesCallback callback) { 592 final ExpandableBinaryDictionary personalizationDict = 593 mDictionaries.getSubDict(Dictionary.TYPE_PERSONALIZATION); 594 if (personalizationDict == null) { 595 if (callback != null) { 596 callback.onFinished(); 597 } 598 return; 599 } 600 final ArrayList<LanguageModelParam> languageModelParams = 601 LanguageModelParam.createLanguageModelParamsFrom( 602 personalizationDataChunk.mTokens, 603 personalizationDataChunk.mTimestampInSeconds, 604 this /* dictionaryFacilitator */, spacingAndPunctuations, 605 new DistracterFilterCheckingIsInDictionary( 606 mDistracterFilter, personalizationDict)); 607 if (languageModelParams == null || languageModelParams.isEmpty()) { 608 if (callback != null) { 609 callback.onFinished(); 610 } 611 return; 612 } 613 personalizationDict.addMultipleDictionaryEntriesDynamically(languageModelParams, callback); 614 } 615 addPhraseToContextualDictionary(final String[] phrase, final int probability, final int bigramProbabilityForWords, final int bigramProbabilityForPhrases)616 public void addPhraseToContextualDictionary(final String[] phrase, final int probability, 617 final int bigramProbabilityForWords, final int bigramProbabilityForPhrases) { 618 final ExpandableBinaryDictionary contextualDict = 619 mDictionaries.getSubDict(Dictionary.TYPE_CONTEXTUAL); 620 if (contextualDict == null) { 621 return; 622 } 623 PrevWordsInfo prevWordsInfo = PrevWordsInfo.BEGINNING_OF_SENTENCE; 624 for (int i = 0; i < phrase.length; i++) { 625 final String[] subPhrase = Arrays.copyOfRange(phrase, i /* start */, phrase.length); 626 final String subPhraseStr = TextUtils.join(Constants.WORD_SEPARATOR, subPhrase); 627 contextualDict.addUnigramEntryWithCheckingDistracter( 628 subPhraseStr, probability, null /* shortcutTarget */, 629 Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, 630 false /* isNotAWord */, false /* isBlacklisted */, 631 BinaryDictionary.NOT_A_VALID_TIMESTAMP, 632 DistracterFilter.EMPTY_DISTRACTER_FILTER); 633 contextualDict.addNgramEntry(prevWordsInfo, subPhraseStr, 634 bigramProbabilityForPhrases, BinaryDictionary.NOT_A_VALID_TIMESTAMP); 635 636 if (i < phrase.length - 1) { 637 contextualDict.addUnigramEntryWithCheckingDistracter( 638 phrase[i], probability, null /* shortcutTarget */, 639 Dictionary.NOT_A_PROBABILITY /* shortcutFreq */, 640 false /* isNotAWord */, false /* isBlacklisted */, 641 BinaryDictionary.NOT_A_VALID_TIMESTAMP, 642 DistracterFilter.EMPTY_DISTRACTER_FILTER); 643 contextualDict.addNgramEntry(prevWordsInfo, phrase[i], 644 bigramProbabilityForWords, BinaryDictionary.NOT_A_VALID_TIMESTAMP); 645 } 646 prevWordsInfo = 647 prevWordsInfo.getNextPrevWordsInfo(new PrevWordsInfo.WordInfo(phrase[i])); 648 } 649 } 650 dumpDictionaryForDebug(final String dictName)651 public void dumpDictionaryForDebug(final String dictName) { 652 final ExpandableBinaryDictionary dictToDump = mDictionaries.getSubDict(dictName); 653 if (dictToDump == null) { 654 Log.e(TAG, "Cannot dump " + dictName + ". " 655 + "The dictionary is not being used for suggestion or cannot be dumped."); 656 return; 657 } 658 dictToDump.dumpAllWordsForDebug(); 659 } 660 } 661