1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * 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 23 import com.android.inputmethod.keyboard.Keyboard; 24 import com.android.inputmethod.keyboard.ProximityInfo; 25 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 26 27 import java.io.File; 28 import java.util.ArrayList; 29 import java.util.HashSet; 30 import java.util.Locale; 31 import java.util.concurrent.ConcurrentHashMap; 32 33 /** 34 * This class loads a dictionary and provides a list of suggestions for a given sequence of 35 * characters. This includes corrections and completions. 36 */ 37 public class Suggest implements Dictionary.WordCallback { 38 public static final String TAG = Suggest.class.getSimpleName(); 39 40 public static final int APPROX_MAX_WORD_LENGTH = 32; 41 42 public static final int CORRECTION_NONE = 0; 43 public static final int CORRECTION_FULL = 1; 44 public static final int CORRECTION_FULL_BIGRAM = 2; 45 46 // It seems the following values are only used for logging. 47 public static final int DIC_USER_TYPED = 0; 48 public static final int DIC_MAIN = 1; 49 public static final int DIC_USER = 2; 50 public static final int DIC_USER_HISTORY = 3; 51 public static final int DIC_CONTACTS = 4; 52 public static final int DIC_WHITELIST = 6; 53 // If you add a type of dictionary, increment DIC_TYPE_LAST_ID 54 // TODO: this value seems unused. Remove it? 55 public static final int DIC_TYPE_LAST_ID = 6; 56 public static final String DICT_KEY_MAIN = "main"; 57 public static final String DICT_KEY_CONTACTS = "contacts"; 58 // User dictionary, the system-managed one. 59 public static final String DICT_KEY_USER = "user"; 60 // User history dictionary for the unigram map, internal to LatinIME 61 public static final String DICT_KEY_USER_HISTORY_UNIGRAM = "history_unigram"; 62 // User history dictionary for the bigram map, internal to LatinIME 63 public static final String DICT_KEY_USER_HISTORY_BIGRAM = "history_bigram"; 64 public static final String DICT_KEY_WHITELIST ="whitelist"; 65 66 private static final boolean DBG = LatinImeLogger.sDBG; 67 68 private boolean mHasMainDictionary; 69 private Dictionary mContactsDict; 70 private WhitelistDictionary mWhiteListDictionary; 71 private final ConcurrentHashMap<String, Dictionary> mUnigramDictionaries = 72 new ConcurrentHashMap<String, Dictionary>(); 73 private final ConcurrentHashMap<String, Dictionary> mBigramDictionaries = 74 new ConcurrentHashMap<String, Dictionary>(); 75 76 private int mPrefMaxSuggestions = 18; 77 78 private static final int PREF_MAX_BIGRAMS = 60; 79 80 private float mAutoCorrectionThreshold; 81 82 private ArrayList<SuggestedWordInfo> mSuggestions = new ArrayList<SuggestedWordInfo>(); 83 private ArrayList<SuggestedWordInfo> mBigramSuggestions = new ArrayList<SuggestedWordInfo>(); 84 private CharSequence mConsideredWord; 85 86 // TODO: Remove these member variables by passing more context to addWord() callback method 87 private boolean mIsFirstCharCapitalized; 88 private boolean mIsAllUpperCase; 89 private int mTrailingSingleQuotesCount; 90 91 private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4; 92 Suggest(final Context context, final Locale locale)93 public Suggest(final Context context, final Locale locale) { 94 initAsynchronously(context, locale); 95 } 96 Suggest(final Context context, final File dictionary, final long startOffset, final long length, final Locale locale)97 /* package for test */ Suggest(final Context context, final File dictionary, 98 final long startOffset, final long length, final Locale locale) { 99 final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary, 100 startOffset, length /* useFullEditDistance */, false, locale); 101 mHasMainDictionary = null != mainDict; 102 addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict); 103 addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict); 104 initWhitelistAndAutocorrectAndPool(context, locale); 105 } 106 initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale)107 private void initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale) { 108 mWhiteListDictionary = new WhitelistDictionary(context, locale); 109 addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_WHITELIST, mWhiteListDictionary); 110 } 111 initAsynchronously(final Context context, final Locale locale)112 private void initAsynchronously(final Context context, final Locale locale) { 113 resetMainDict(context, locale); 114 115 // TODO: read the whitelist and init the pool asynchronously too. 116 // initPool should be done asynchronously now that the pool is thread-safe. 117 initWhitelistAndAutocorrectAndPool(context, locale); 118 } 119 addOrReplaceDictionary( final ConcurrentHashMap<String, Dictionary> dictionaries, final String key, final Dictionary dict)120 private static void addOrReplaceDictionary( 121 final ConcurrentHashMap<String, Dictionary> dictionaries, 122 final String key, final Dictionary dict) { 123 final Dictionary oldDict = (dict == null) 124 ? dictionaries.remove(key) 125 : dictionaries.put(key, dict); 126 if (oldDict != null && dict != oldDict) { 127 oldDict.close(); 128 } 129 } 130 resetMainDict(final Context context, final Locale locale)131 public void resetMainDict(final Context context, final Locale locale) { 132 mHasMainDictionary = false; 133 new Thread("InitializeBinaryDictionary") { 134 @Override 135 public void run() { 136 final DictionaryCollection newMainDict = 137 DictionaryFactory.createMainDictionaryFromManager(context, locale); 138 mHasMainDictionary = null != newMainDict && !newMainDict.isEmpty(); 139 addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict); 140 addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict); 141 } 142 }.start(); 143 } 144 145 // The main dictionary could have been loaded asynchronously. Don't cache the return value 146 // of this method. hasMainDictionary()147 public boolean hasMainDictionary() { 148 return mHasMainDictionary; 149 } 150 getContactsDictionary()151 public Dictionary getContactsDictionary() { 152 return mContactsDict; 153 } 154 getUnigramDictionaries()155 public ConcurrentHashMap<String, Dictionary> getUnigramDictionaries() { 156 return mUnigramDictionaries; 157 } 158 getApproxMaxWordLength()159 public static int getApproxMaxWordLength() { 160 return APPROX_MAX_WORD_LENGTH; 161 } 162 163 /** 164 * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted 165 * before the main dictionary, if set. This refers to the system-managed user dictionary. 166 */ setUserDictionary(Dictionary userDictionary)167 public void setUserDictionary(Dictionary userDictionary) { 168 addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER, userDictionary); 169 } 170 171 /** 172 * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove 173 * the contacts dictionary by passing null to this method. In this case no contacts dictionary 174 * won't be used. 175 */ setContactsDictionary(Dictionary contactsDictionary)176 public void setContactsDictionary(Dictionary contactsDictionary) { 177 mContactsDict = contactsDictionary; 178 addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary); 179 addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary); 180 } 181 setUserHistoryDictionary(Dictionary userHistoryDictionary)182 public void setUserHistoryDictionary(Dictionary userHistoryDictionary) { 183 addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER_HISTORY_UNIGRAM, 184 userHistoryDictionary); 185 addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_HISTORY_BIGRAM, 186 userHistoryDictionary); 187 } 188 setAutoCorrectionThreshold(float threshold)189 public void setAutoCorrectionThreshold(float threshold) { 190 mAutoCorrectionThreshold = threshold; 191 } 192 capitalizeWord(final boolean all, final boolean first, final CharSequence word)193 private static CharSequence capitalizeWord(final boolean all, final boolean first, 194 final CharSequence word) { 195 if (TextUtils.isEmpty(word) || !(all || first)) return word; 196 final int wordLength = word.length(); 197 final StringBuilder sb = new StringBuilder(getApproxMaxWordLength()); 198 // TODO: Must pay attention to locale when changing case. 199 if (all) { 200 sb.append(word.toString().toUpperCase()); 201 } else if (first) { 202 sb.append(Character.toUpperCase(word.charAt(0))); 203 if (wordLength > 1) { 204 sb.append(word.subSequence(1, wordLength)); 205 } 206 } 207 return sb; 208 } 209 addBigramToSuggestions(SuggestedWordInfo bigram)210 protected void addBigramToSuggestions(SuggestedWordInfo bigram) { 211 mSuggestions.add(bigram); 212 } 213 214 private static final WordComposer sEmptyWordComposer = new WordComposer(); getBigramPredictions(CharSequence prevWordForBigram)215 public SuggestedWords getBigramPredictions(CharSequence prevWordForBigram) { 216 LatinImeLogger.onStartSuggestion(prevWordForBigram); 217 mIsFirstCharCapitalized = false; 218 mIsAllUpperCase = false; 219 mTrailingSingleQuotesCount = 0; 220 mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions); 221 222 // Treating USER_TYPED as UNIGRAM suggestion for logging now. 223 LatinImeLogger.onAddSuggestedWord("", Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM); 224 mConsideredWord = ""; 225 226 mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS); 227 228 getAllBigrams(prevWordForBigram, sEmptyWordComposer); 229 230 // Nothing entered: return all bigrams for the previous word 231 int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions); 232 for (int i = 0; i < insertCount; ++i) { 233 addBigramToSuggestions(mBigramSuggestions.get(i)); 234 } 235 236 SuggestedWordInfo.removeDups(mSuggestions); 237 238 return new SuggestedWords(mSuggestions, 239 false /* typedWordValid */, 240 false /* hasAutoCorrectionCandidate */, 241 false /* allowsToBeAutoCorrected */, 242 false /* isPunctuationSuggestions */, 243 false /* isObsoleteSuggestions */, 244 true /* isPrediction */); 245 } 246 247 // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder getSuggestedWords( final WordComposer wordComposer, CharSequence prevWordForBigram, final ProximityInfo proximityInfo, final int correctionMode)248 public SuggestedWords getSuggestedWords( 249 final WordComposer wordComposer, CharSequence prevWordForBigram, 250 final ProximityInfo proximityInfo, final int correctionMode) { 251 LatinImeLogger.onStartSuggestion(prevWordForBigram); 252 mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized(); 253 mIsAllUpperCase = wordComposer.isAllUpperCase(); 254 mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount(); 255 mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions); 256 257 final String typedWord = wordComposer.getTypedWord(); 258 final String consideredWord = mTrailingSingleQuotesCount > 0 259 ? typedWord.substring(0, typedWord.length() - mTrailingSingleQuotesCount) 260 : typedWord; 261 // Treating USER_TYPED as UNIGRAM suggestion for logging now. 262 LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM); 263 mConsideredWord = consideredWord; 264 265 if (wordComposer.size() <= 1 && (correctionMode == CORRECTION_FULL_BIGRAM)) { 266 // At first character typed, search only the bigrams 267 mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS); 268 269 if (!TextUtils.isEmpty(prevWordForBigram)) { 270 getAllBigrams(prevWordForBigram, wordComposer); 271 if (TextUtils.isEmpty(consideredWord)) { 272 // Nothing entered: return all bigrams for the previous word 273 int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions); 274 for (int i = 0; i < insertCount; ++i) { 275 addBigramToSuggestions(mBigramSuggestions.get(i)); 276 } 277 } else { 278 // Word entered: return only bigrams that match the first char of the typed word 279 final char currentChar = consideredWord.charAt(0); 280 // TODO: Must pay attention to locale when changing case. 281 // TODO: Use codepoint instead of char 282 final char currentCharUpper = Character.toUpperCase(currentChar); 283 int count = 0; 284 final int bigramSuggestionSize = mBigramSuggestions.size(); 285 for (int i = 0; i < bigramSuggestionSize; i++) { 286 final SuggestedWordInfo bigramSuggestion = mBigramSuggestions.get(i); 287 final char bigramSuggestionFirstChar = 288 (char)bigramSuggestion.codePointAt(0); 289 if (bigramSuggestionFirstChar == currentChar 290 || bigramSuggestionFirstChar == currentCharUpper) { 291 addBigramToSuggestions(bigramSuggestion); 292 if (++count > mPrefMaxSuggestions) break; 293 } 294 } 295 } 296 } 297 298 } else if (wordComposer.size() > 1) { 299 final WordComposer wordComposerForLookup; 300 if (mTrailingSingleQuotesCount > 0) { 301 wordComposerForLookup = new WordComposer(wordComposer); 302 for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { 303 wordComposerForLookup.deleteLast(); 304 } 305 } else { 306 wordComposerForLookup = wordComposer; 307 } 308 // At second character typed, search the unigrams (scores being affected by bigrams) 309 for (final String key : mUnigramDictionaries.keySet()) { 310 // Skip UserUnigramDictionary and WhitelistDictionary to lookup 311 if (key.equals(DICT_KEY_USER_HISTORY_UNIGRAM) || key.equals(DICT_KEY_WHITELIST)) 312 continue; 313 final Dictionary dictionary = mUnigramDictionaries.get(key); 314 dictionary.getWords(wordComposerForLookup, prevWordForBigram, this, proximityInfo); 315 } 316 } 317 318 final CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase, 319 mIsFirstCharCapitalized, mWhiteListDictionary.getWhitelistedWord(consideredWord)); 320 321 final boolean hasAutoCorrection; 322 if (CORRECTION_FULL == correctionMode || CORRECTION_FULL_BIGRAM == correctionMode) { 323 final CharSequence autoCorrection = 324 AutoCorrection.computeAutoCorrectionWord(mUnigramDictionaries, wordComposer, 325 mSuggestions, consideredWord, mAutoCorrectionThreshold, 326 whitelistedWord); 327 hasAutoCorrection = (null != autoCorrection); 328 } else { 329 hasAutoCorrection = false; 330 } 331 332 if (whitelistedWord != null) { 333 if (mTrailingSingleQuotesCount > 0) { 334 final StringBuilder sb = new StringBuilder(whitelistedWord); 335 for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { 336 sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE); 337 } 338 mSuggestions.add(0, new SuggestedWordInfo( 339 sb.toString(), SuggestedWordInfo.MAX_SCORE)); 340 } else { 341 mSuggestions.add(0, new SuggestedWordInfo( 342 whitelistedWord, SuggestedWordInfo.MAX_SCORE)); 343 } 344 } 345 346 mSuggestions.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE)); 347 SuggestedWordInfo.removeDups(mSuggestions); 348 349 final ArrayList<SuggestedWordInfo> suggestionsList; 350 if (DBG) { 351 suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, mSuggestions); 352 } else { 353 suggestionsList = mSuggestions; 354 } 355 356 // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid" 357 // but still autocorrected from - in the case the whitelist only capitalizes the word. 358 // The whitelist should be case-insensitive, so it's not possible to be consistent with 359 // a boolean flag. Right now this is handled with a slight hack in 360 // WhitelistDictionary#shouldForciblyAutoCorrectFrom. 361 final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected( 362 getUnigramDictionaries(), consideredWord, wordComposer.isFirstCharCapitalized()) 363 // If we don't have a main dictionary, we never want to auto-correct. The reason for this 364 // is, the user may have a contact whose name happens to match a valid word in their 365 // language, and it will unexpectedly auto-correct. For example, if the user types in 366 // English with no dictionary and has a "Will" in their contact list, "will" would 367 // always auto-correct to "Will" which is unwanted. Hence, no main dict => no auto-correct. 368 && mHasMainDictionary; 369 370 boolean autoCorrectionAvailable = hasAutoCorrection; 371 if (correctionMode == CORRECTION_FULL || correctionMode == CORRECTION_FULL_BIGRAM) { 372 autoCorrectionAvailable |= !allowsToBeAutoCorrected; 373 } 374 // Don't auto-correct words with multiple capital letter 375 autoCorrectionAvailable &= !wordComposer.isMostlyCaps(); 376 autoCorrectionAvailable &= !wordComposer.isResumed(); 377 if (allowsToBeAutoCorrected && suggestionsList.size() > 1 && mAutoCorrectionThreshold > 0 378 && Suggest.shouldBlockAutoCorrectionBySafetyNet(typedWord, 379 suggestionsList.get(1).mWord)) { 380 autoCorrectionAvailable = false; 381 } 382 return new SuggestedWords(suggestionsList, 383 !allowsToBeAutoCorrected /* typedWordValid */, 384 autoCorrectionAvailable /* hasAutoCorrectionCandidate */, 385 allowsToBeAutoCorrected /* allowsToBeAutoCorrected */, 386 false /* isPunctuationSuggestions */, 387 false /* isObsoleteSuggestions */, 388 false /* isPrediction */); 389 } 390 391 /** 392 * Adds all bigram predictions for prevWord. Also checks the lower case version of prevWord if 393 * it contains any upper case characters. 394 */ getAllBigrams(final CharSequence prevWord, final WordComposer wordComposer)395 private void getAllBigrams(final CharSequence prevWord, final WordComposer wordComposer) { 396 if (StringUtils.hasUpperCase(prevWord)) { 397 // TODO: Must pay attention to locale when changing case. 398 final CharSequence lowerPrevWord = prevWord.toString().toLowerCase(); 399 for (final Dictionary dictionary : mBigramDictionaries.values()) { 400 dictionary.getBigrams(wordComposer, lowerPrevWord, this); 401 } 402 } 403 for (final Dictionary dictionary : mBigramDictionaries.values()) { 404 dictionary.getBigrams(wordComposer, prevWord, this); 405 } 406 } 407 getSuggestionsInfoListWithDebugInfo( final String typedWord, final ArrayList<SuggestedWordInfo> suggestions)408 private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo( 409 final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) { 410 final SuggestedWordInfo typedWordInfo = suggestions.get(0); 411 typedWordInfo.setDebugString("+"); 412 final int suggestionsSize = suggestions.size(); 413 final ArrayList<SuggestedWordInfo> suggestionsList = 414 new ArrayList<SuggestedWordInfo>(suggestionsSize); 415 suggestionsList.add(typedWordInfo); 416 // Note: i here is the index in mScores[], but the index in mSuggestions is one more 417 // than i because we added the typed word to mSuggestions without touching mScores. 418 for (int i = 0; i < suggestionsSize - 1; ++i) { 419 final SuggestedWordInfo cur = suggestions.get(i + 1); 420 final float normalizedScore = BinaryDictionary.calcNormalizedScore( 421 typedWord, cur.toString(), cur.mScore); 422 final String scoreInfoString; 423 if (normalizedScore > 0) { 424 scoreInfoString = String.format("%d (%4.2f)", cur.mScore, normalizedScore); 425 } else { 426 scoreInfoString = Integer.toString(cur.mScore); 427 } 428 cur.setDebugString(scoreInfoString); 429 suggestionsList.add(cur); 430 } 431 return suggestionsList; 432 } 433 434 // TODO: Use codepoint instead of char 435 @Override addWord(final char[] word, final int offset, final int length, int score, final int dicTypeId, final int dataType)436 public boolean addWord(final char[] word, final int offset, final int length, int score, 437 final int dicTypeId, final int dataType) { 438 int dataTypeForLog = dataType; 439 final ArrayList<SuggestedWordInfo> suggestions; 440 final int prefMaxSuggestions; 441 if (dataType == Dictionary.BIGRAM) { 442 suggestions = mBigramSuggestions; 443 prefMaxSuggestions = PREF_MAX_BIGRAMS; 444 } else { 445 suggestions = mSuggestions; 446 prefMaxSuggestions = mPrefMaxSuggestions; 447 } 448 449 int pos = 0; 450 451 // Check if it's the same word, only caps are different 452 if (StringUtils.equalsIgnoreCase(mConsideredWord, word, offset, length)) { 453 // TODO: remove this surrounding if clause and move this logic to 454 // getSuggestedWordBuilder. 455 if (suggestions.size() > 0) { 456 final SuggestedWordInfo currentHighestWord = suggestions.get(0); 457 // If the current highest word is also equal to typed word, we need to compare 458 // frequency to determine the insertion position. This does not ensure strictly 459 // correct ordering, but ensures the top score is on top which is enough for 460 // removing duplicates correctly. 461 if (StringUtils.equalsIgnoreCase(currentHighestWord.mWord, word, offset, length) 462 && score <= currentHighestWord.mScore) { 463 pos = 1; 464 } 465 } 466 } else { 467 // Check the last one's score and bail 468 if (suggestions.size() >= prefMaxSuggestions 469 && suggestions.get(prefMaxSuggestions - 1).mScore >= score) return true; 470 while (pos < suggestions.size()) { 471 final int curScore = suggestions.get(pos).mScore; 472 if (curScore < score 473 || (curScore == score && length < suggestions.get(pos).codePointCount())) { 474 break; 475 } 476 pos++; 477 } 478 } 479 if (pos >= prefMaxSuggestions) { 480 return true; 481 } 482 483 final StringBuilder sb = new StringBuilder(getApproxMaxWordLength()); 484 // TODO: Must pay attention to locale when changing case. 485 if (mIsAllUpperCase) { 486 sb.append(new String(word, offset, length).toUpperCase()); 487 } else if (mIsFirstCharCapitalized) { 488 sb.append(Character.toUpperCase(word[offset])); 489 if (length > 1) { 490 sb.append(word, offset + 1, length - 1); 491 } 492 } else { 493 sb.append(word, offset, length); 494 } 495 for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) { 496 sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE); 497 } 498 suggestions.add(pos, new SuggestedWordInfo(sb, score)); 499 if (suggestions.size() > prefMaxSuggestions) { 500 suggestions.remove(prefMaxSuggestions); 501 } else { 502 LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog); 503 } 504 return true; 505 } 506 close()507 public void close() { 508 final HashSet<Dictionary> dictionaries = new HashSet<Dictionary>(); 509 dictionaries.addAll(mUnigramDictionaries.values()); 510 dictionaries.addAll(mBigramDictionaries.values()); 511 for (final Dictionary dictionary : dictionaries) { 512 dictionary.close(); 513 } 514 mHasMainDictionary = false; 515 } 516 517 // TODO: Resolve the inconsistencies between the native auto correction algorithms and 518 // this safety net shouldBlockAutoCorrectionBySafetyNet(final String typedWord, final CharSequence suggestion)519 public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord, 520 final CharSequence suggestion) { 521 // Safety net for auto correction. 522 // Actually if we hit this safety net, it's a bug. 523 // If user selected aggressive auto correction mode, there is no need to use the safety 524 // net. 525 // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH, 526 // we should not use net because relatively edit distance can be big. 527 final int typedWordLength = typedWord.length(); 528 if (typedWordLength < Suggest.MINIMUM_SAFETY_NET_CHAR_LENGTH) { 529 return false; 530 } 531 final int maxEditDistanceOfNativeDictionary = 532 (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1; 533 final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString()); 534 if (DBG) { 535 Log.d(TAG, "Autocorrected edit distance = " + distance 536 + ", " + maxEditDistanceOfNativeDictionary); 537 } 538 if (distance > maxEditDistanceOfNativeDictionary) { 539 if (DBG) { 540 Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion); 541 Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. " 542 + "Turning off auto-correction."); 543 } 544 return true; 545 } else { 546 return false; 547 } 548 } 549 } 550