AndroidSpellCheckerService.java revision 1830cd1dc8259aa57175f1cf2a3d8797a7a35935
1/* 2 * Copyright (C) 2011 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 17package com.android.inputmethod.latin.spellcheck; 18 19import android.content.Intent; 20import android.content.SharedPreferences; 21import android.content.res.Resources; 22import android.preference.PreferenceManager; 23import android.service.textservice.SpellCheckerService; 24import android.text.TextUtils; 25import android.util.Log; 26import android.view.textservice.SuggestionsInfo; 27import android.view.textservice.TextInfo; 28 29import com.android.inputmethod.compat.ArraysCompatUtils; 30import com.android.inputmethod.keyboard.ProximityInfo; 31import com.android.inputmethod.latin.BinaryDictionary; 32import com.android.inputmethod.latin.Dictionary; 33import com.android.inputmethod.latin.Dictionary.DataType; 34import com.android.inputmethod.latin.Dictionary.WordCallback; 35import com.android.inputmethod.latin.DictionaryCollection; 36import com.android.inputmethod.latin.DictionaryFactory; 37import com.android.inputmethod.latin.Flag; 38import com.android.inputmethod.latin.LocaleUtils; 39import com.android.inputmethod.latin.R; 40import com.android.inputmethod.latin.SynchronouslyLoadedContactsDictionary; 41import com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary; 42import com.android.inputmethod.latin.Utils; 43import com.android.inputmethod.latin.WhitelistDictionary; 44import com.android.inputmethod.latin.WordComposer; 45 46import java.lang.ref.WeakReference; 47import java.util.ArrayList; 48import java.util.Arrays; 49import java.util.Collections; 50import java.util.Iterator; 51import java.util.Locale; 52import java.util.Map; 53import java.util.TreeMap; 54import java.util.HashSet; 55 56/** 57 * Service for spell checking, using LatinIME's dictionaries and mechanisms. 58 */ 59public class AndroidSpellCheckerService extends SpellCheckerService 60 implements SharedPreferences.OnSharedPreferenceChangeListener { 61 private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); 62 private static final boolean DBG = false; 63 private static final int POOL_SIZE = 2; 64 65 public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts"; 66 67 private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case 68 private static final int CAPITALIZE_FIRST = 1; // First only 69 private static final int CAPITALIZE_ALL = 2; // All caps 70 71 private final static String[] EMPTY_STRING_ARRAY = new String[0]; 72 private final static Flag[] USE_FULL_EDIT_DISTANCE_FLAG_ARRAY; 73 static { 74 // See BinaryDictionary.java for an explanation of these flags 75 // Specifially, ALL_CONFIG_FLAGS means that we want to consider all flags with the 76 // current dictionary configuration - for example, consider the UMLAUT flag 77 // so that it will be turned on for German dictionaries and off for others. 78 USE_FULL_EDIT_DISTANCE_FLAG_ARRAY = Arrays.copyOf(BinaryDictionary.ALL_CONFIG_FLAGS, 79 BinaryDictionary.ALL_CONFIG_FLAGS.length + 1); 80 USE_FULL_EDIT_DISTANCE_FLAG_ARRAY[BinaryDictionary.ALL_CONFIG_FLAGS.length] = 81 BinaryDictionary.FLAG_USE_FULL_EDIT_DISTANCE; 82 } 83 private Map<String, DictionaryPool> mDictionaryPools = 84 Collections.synchronizedMap(new TreeMap<String, DictionaryPool>()); 85 private Map<String, Dictionary> mUserDictionaries = 86 Collections.synchronizedMap(new TreeMap<String, Dictionary>()); 87 private Map<String, Dictionary> mWhitelistDictionaries = 88 Collections.synchronizedMap(new TreeMap<String, Dictionary>()); 89 private SynchronouslyLoadedContactsDictionary mContactsDictionary; 90 91 // The threshold for a candidate to be offered as a suggestion. 92 private double mSuggestionThreshold; 93 // The threshold for a suggestion to be considered "recommended". 94 private double mRecommendedThreshold; 95 // Whether to use the contacts dictionary 96 private boolean mUseContactsDictionary; 97 private final Object mUseContactsLock = new Object(); 98 99 private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList = 100 new HashSet<WeakReference<DictionaryCollection>>(); 101 102 public static final int SCRIPT_LATIN = 0; 103 public static final int SCRIPT_CYRILLIC = 1; 104 private static final TreeMap<String, Integer> mLanguageToScript; 105 static { 106 // List of the supported languages and their associated script. We won't check 107 // words written in another script than the selected script, because we know we 108 // don't have those in our dictionary so we will underline everything and we 109 // will never have any suggestions, so it makes no sense checking them. 110 mLanguageToScript = new TreeMap<String, Integer>(); 111 mLanguageToScript.put("en", SCRIPT_LATIN); 112 mLanguageToScript.put("fr", SCRIPT_LATIN); 113 mLanguageToScript.put("de", SCRIPT_LATIN); 114 mLanguageToScript.put("nl", SCRIPT_LATIN); 115 mLanguageToScript.put("cs", SCRIPT_LATIN); 116 mLanguageToScript.put("es", SCRIPT_LATIN); 117 mLanguageToScript.put("it", SCRIPT_LATIN); 118 mLanguageToScript.put("ru", SCRIPT_CYRILLIC); 119 } 120 121 @Override public void onCreate() { 122 super.onCreate(); 123 mSuggestionThreshold = 124 Double.parseDouble(getString(R.string.spellchecker_suggestion_threshold_value)); 125 mRecommendedThreshold = 126 Double.parseDouble(getString(R.string.spellchecker_recommended_threshold_value)); 127 final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 128 prefs.registerOnSharedPreferenceChangeListener(this); 129 onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY); 130 } 131 132 private static int getScriptFromLocale(final Locale locale) { 133 final Integer script = mLanguageToScript.get(locale.getLanguage()); 134 if (null == script) { 135 throw new RuntimeException("We have been called with an unsupported language: \"" 136 + locale.getLanguage() + "\". Framework bug?"); 137 } 138 return script; 139 } 140 141 @Override 142 public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { 143 if (!PREF_USE_CONTACTS_KEY.equals(key)) return; 144 synchronized(mUseContactsLock) { 145 mUseContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true); 146 if (mUseContactsDictionary) { 147 startUsingContactsDictionaryLocked(); 148 } else { 149 stopUsingContactsDictionaryLocked(); 150 } 151 } 152 } 153 154 private void startUsingContactsDictionaryLocked() { 155 if (null == mContactsDictionary) { 156 mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this); 157 } 158 final Iterator<WeakReference<DictionaryCollection>> iterator = 159 mDictionaryCollectionsList.iterator(); 160 while (iterator.hasNext()) { 161 final WeakReference<DictionaryCollection> dictRef = iterator.next(); 162 final DictionaryCollection dict = dictRef.get(); 163 if (null == dict) { 164 iterator.remove(); 165 } else { 166 dict.addDictionary(mContactsDictionary); 167 } 168 } 169 } 170 171 private void stopUsingContactsDictionaryLocked() { 172 if (null == mContactsDictionary) return; 173 final SynchronouslyLoadedContactsDictionary contactsDict = mContactsDictionary; 174 mContactsDictionary = null; 175 final Iterator<WeakReference<DictionaryCollection>> iterator = 176 mDictionaryCollectionsList.iterator(); 177 while (iterator.hasNext()) { 178 final WeakReference<DictionaryCollection> dictRef = iterator.next(); 179 final DictionaryCollection dict = dictRef.get(); 180 if (null == dict) { 181 iterator.remove(); 182 } else { 183 dict.removeDictionary(contactsDict); 184 } 185 } 186 contactsDict.close(); 187 } 188 189 @Override 190 public Session createSession() { 191 return new AndroidSpellCheckerSession(this); 192 } 193 194 private static SuggestionsInfo getNotInDictEmptySuggestions() { 195 return new SuggestionsInfo(0, EMPTY_STRING_ARRAY); 196 } 197 198 private static SuggestionsInfo getInDictEmptySuggestions() { 199 return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY, 200 EMPTY_STRING_ARRAY); 201 } 202 203 private static class SuggestionsGatherer implements WordCallback { 204 public static class Result { 205 public final String[] mSuggestions; 206 public final boolean mHasRecommendedSuggestions; 207 public Result(final String[] gatheredSuggestions, 208 final boolean hasRecommendedSuggestions) { 209 mSuggestions = gatheredSuggestions; 210 mHasRecommendedSuggestions = hasRecommendedSuggestions; 211 } 212 } 213 214 private final ArrayList<CharSequence> mSuggestions; 215 private final int[] mScores; 216 private final String mOriginalText; 217 private final double mSuggestionThreshold; 218 private final double mRecommendedThreshold; 219 private final int mMaxLength; 220 private int mLength = 0; 221 222 // The two following attributes are only ever filled if the requested max length 223 // is 0 (or less, which is treated the same). 224 private String mBestSuggestion = null; 225 private int mBestScore = Integer.MIN_VALUE; // As small as possible 226 227 SuggestionsGatherer(final String originalText, final double suggestionThreshold, 228 final double recommendedThreshold, final int maxLength) { 229 mOriginalText = originalText; 230 mSuggestionThreshold = suggestionThreshold; 231 mRecommendedThreshold = recommendedThreshold; 232 mMaxLength = maxLength; 233 mSuggestions = new ArrayList<CharSequence>(maxLength + 1); 234 mScores = new int[mMaxLength]; 235 } 236 237 @Override 238 synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score, 239 int dicTypeId, DataType dataType) { 240 final int positionIndex = ArraysCompatUtils.binarySearch(mScores, 0, mLength, score); 241 // binarySearch returns the index if the element exists, and -<insertion index> - 1 242 // if it doesn't. See documentation for binarySearch. 243 final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1; 244 245 if (insertIndex == 0 && mLength >= mMaxLength) { 246 // In the future, we may want to keep track of the best suggestion score even if 247 // we are asked for 0 suggestions. In this case, we can use the following 248 // (tested) code to keep it: 249 // If the maxLength is 0 (should never be less, but if it is, it's treated as 0) 250 // then we need to keep track of the best suggestion in mBestScore and 251 // mBestSuggestion. This is so that we know whether the best suggestion makes 252 // the score cutoff, since we need to know that to return a meaningful 253 // looksLikeTypo. 254 // if (0 >= mMaxLength) { 255 // if (score > mBestScore) { 256 // mBestScore = score; 257 // mBestSuggestion = new String(word, wordOffset, wordLength); 258 // } 259 // } 260 return true; 261 } 262 if (insertIndex >= mMaxLength) { 263 // We found a suggestion, but its score is too weak to be kept considering 264 // the suggestion limit. 265 return true; 266 } 267 268 // Compute the normalized score and skip this word if it's normalized score does not 269 // make the threshold. 270 final String wordString = new String(word, wordOffset, wordLength); 271 final double normalizedScore = 272 Utils.calcNormalizedScore(mOriginalText, wordString, score); 273 if (normalizedScore < mSuggestionThreshold) { 274 if (DBG) Log.i(TAG, wordString + " does not make the score threshold"); 275 return true; 276 } 277 278 if (mLength < mMaxLength) { 279 final int copyLen = mLength - insertIndex; 280 ++mLength; 281 System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen); 282 mSuggestions.add(insertIndex, wordString); 283 } else { 284 System.arraycopy(mScores, 1, mScores, 0, insertIndex); 285 mSuggestions.add(insertIndex, wordString); 286 mSuggestions.remove(0); 287 } 288 mScores[insertIndex] = score; 289 290 return true; 291 } 292 293 public Result getResults(final int capitalizeType, final Locale locale) { 294 final String[] gatheredSuggestions; 295 final boolean hasRecommendedSuggestions; 296 if (0 == mLength) { 297 // Either we found no suggestions, or we found some BUT the max length was 0. 298 // If we found some mBestSuggestion will not be null. If it is null, then 299 // we found none, regardless of the max length. 300 if (null == mBestSuggestion) { 301 gatheredSuggestions = null; 302 hasRecommendedSuggestions = false; 303 } else { 304 gatheredSuggestions = EMPTY_STRING_ARRAY; 305 final double normalizedScore = 306 Utils.calcNormalizedScore(mOriginalText, mBestSuggestion, mBestScore); 307 hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); 308 } 309 } else { 310 if (DBG) { 311 if (mLength != mSuggestions.size()) { 312 Log.e(TAG, "Suggestion size is not the same as stored mLength"); 313 } 314 for (int i = mLength - 1; i >= 0; --i) { 315 Log.i(TAG, "" + mScores[i] + " " + mSuggestions.get(i)); 316 } 317 } 318 Collections.reverse(mSuggestions); 319 Utils.removeDupes(mSuggestions); 320 if (CAPITALIZE_ALL == capitalizeType) { 321 for (int i = 0; i < mSuggestions.size(); ++i) { 322 // get(i) returns a CharSequence which is actually a String so .toString() 323 // should return the same object. 324 mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale)); 325 } 326 } else if (CAPITALIZE_FIRST == capitalizeType) { 327 for (int i = 0; i < mSuggestions.size(); ++i) { 328 // Likewise 329 mSuggestions.set(i, Utils.toTitleCase(mSuggestions.get(i).toString(), 330 locale)); 331 } 332 } 333 // This returns a String[], while toArray() returns an Object[] which cannot be cast 334 // into a String[]. 335 gatheredSuggestions = mSuggestions.toArray(EMPTY_STRING_ARRAY); 336 337 final int bestScore = mScores[mLength - 1]; 338 final CharSequence bestSuggestion = mSuggestions.get(0); 339 final double normalizedScore = 340 Utils.calcNormalizedScore(mOriginalText, bestSuggestion, bestScore); 341 hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold); 342 if (DBG) { 343 Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore); 344 Log.i(TAG, "Normalized score = " + normalizedScore 345 + " (threshold " + mRecommendedThreshold 346 + ") => hasRecommendedSuggestions = " + hasRecommendedSuggestions); 347 } 348 } 349 return new Result(gatheredSuggestions, hasRecommendedSuggestions); 350 } 351 } 352 353 @Override 354 public boolean onUnbind(final Intent intent) { 355 final Map<String, DictionaryPool> oldPools = mDictionaryPools; 356 mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>()); 357 final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries; 358 mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>()); 359 final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries; 360 mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>()); 361 for (DictionaryPool pool : oldPools.values()) { 362 pool.close(); 363 } 364 for (Dictionary dict : oldUserDictionaries.values()) { 365 dict.close(); 366 } 367 for (Dictionary dict : oldWhitelistDictionaries.values()) { 368 dict.close(); 369 } 370 synchronized(mUseContactsLock) { 371 if (null != mContactsDictionary) { 372 // The synchronously loaded contacts dictionary should have been in one 373 // or several pools, but it is shielded against multiple closing and it's 374 // safe to call it several times. 375 final SynchronouslyLoadedContactsDictionary dictToClose = mContactsDictionary; 376 mContactsDictionary = null; 377 dictToClose.close(); 378 } 379 } 380 return false; 381 } 382 383 private DictionaryPool getDictionaryPool(final String locale) { 384 DictionaryPool pool = mDictionaryPools.get(locale); 385 if (null == pool) { 386 final Locale localeObject = LocaleUtils.constructLocaleFromString(locale); 387 pool = new DictionaryPool(POOL_SIZE, this, localeObject); 388 mDictionaryPools.put(locale, pool); 389 } 390 return pool; 391 } 392 393 public DictAndProximity createDictAndProximity(final Locale locale) { 394 final int script = getScriptFromLocale(locale); 395 final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo( 396 SpellCheckerProximityInfo.getProximityForScript(script)); 397 final Resources resources = getResources(); 398 final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources); 399 final DictionaryCollection dictionaryCollection = 400 DictionaryFactory.createDictionaryFromManager(this, locale, fallbackResourceId, 401 USE_FULL_EDIT_DISTANCE_FLAG_ARRAY); 402 final String localeStr = locale.toString(); 403 Dictionary userDictionary = mUserDictionaries.get(localeStr); 404 if (null == userDictionary) { 405 userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true); 406 mUserDictionaries.put(localeStr, userDictionary); 407 } 408 dictionaryCollection.addDictionary(userDictionary); 409 Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr); 410 if (null == whitelistDictionary) { 411 whitelistDictionary = new WhitelistDictionary(this, locale); 412 mWhitelistDictionaries.put(localeStr, whitelistDictionary); 413 } 414 dictionaryCollection.addDictionary(whitelistDictionary); 415 synchronized(mUseContactsLock) { 416 if (mUseContactsDictionary) { 417 if (null == mContactsDictionary) { 418 mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this); 419 } 420 } 421 dictionaryCollection.addDictionary(mContactsDictionary); 422 mDictionaryCollectionsList.add( 423 new WeakReference<DictionaryCollection>(dictionaryCollection)); 424 } 425 return new DictAndProximity(dictionaryCollection, proximityInfo); 426 } 427 428 // This method assumes the text is not empty or null. 429 private static int getCapitalizationType(String text) { 430 // If the first char is not uppercase, then the word is either all lower case, 431 // and in either case we return CAPITALIZE_NONE. 432 if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE; 433 final int len = text.codePointCount(0, text.length()); 434 int capsCount = 1; 435 for (int i = 1; i < len; ++i) { 436 if (1 != capsCount && i != capsCount) break; 437 if (Character.isUpperCase(text.codePointAt(i))) ++capsCount; 438 } 439 // We know the first char is upper case. So we want to test if either everything 440 // else is lower case, or if everything else is upper case. If the string is 441 // exactly one char long, then we will arrive here with capsCount 1, and this is 442 // correct, too. 443 if (1 == capsCount) return CAPITALIZE_FIRST; 444 return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE); 445 } 446 447 private static class AndroidSpellCheckerSession extends Session { 448 // Immutable, but need the locale which is not available in the constructor yet 449 private DictionaryPool mDictionaryPool; 450 // Likewise 451 private Locale mLocale; 452 // Cache this for performance 453 private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now. 454 455 private final AndroidSpellCheckerService mService; 456 457 AndroidSpellCheckerSession(final AndroidSpellCheckerService service) { 458 mService = service; 459 } 460 461 @Override 462 public void onCreate() { 463 final String localeString = getLocale(); 464 mDictionaryPool = mService.getDictionaryPool(localeString); 465 mLocale = LocaleUtils.constructLocaleFromString(localeString); 466 mScript = getScriptFromLocale(mLocale); 467 } 468 469 /* 470 * Returns whether the code point is a letter that makes sense for the specified 471 * locale for this spell checker. 472 * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml 473 * and is limited to EFIGS languages and Russian. 474 * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters 475 * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters. 476 */ 477 private static boolean isLetterCheckableByLanguage(final int codePoint, 478 final int script) { 479 switch (script) { 480 case SCRIPT_LATIN: 481 // Our supported latin script dictionaries (EFIGS) at the moment only include 482 // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode 483 // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF, 484 // so the below is a very efficient way to test for it. As for the 0-0x3F, it's 485 // excluded from isLetter anyway. 486 return codePoint <= 0x2AF && Character.isLetter(codePoint); 487 case SCRIPT_CYRILLIC: 488 // All Cyrillic characters are in the 400~52F block. There are some in the upper 489 // Unicode range, but they are archaic characters that are not used in modern 490 // russian and are not used by our dictionary. 491 return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint); 492 default: 493 // Should never come here 494 throw new RuntimeException("Impossible value of script: " + script); 495 } 496 } 497 498 /** 499 * Finds out whether a particular string should be filtered out of spell checking. 500 * 501 * This will loosely match URLs, numbers, symbols. To avoid always underlining words that 502 * we know we will never recognize, this accepts a script identifier that should be one 503 * of the SCRIPT_* constants defined above, to rule out quickly characters from very 504 * different languages. 505 * 506 * @param text the string to evaluate. 507 * @param script the identifier for the script this spell checker recognizes 508 * @return true if we should filter this text out, false otherwise 509 */ 510 private static boolean shouldFilterOut(final String text, final int script) { 511 if (TextUtils.isEmpty(text) || text.length() <= 1) return true; 512 513 // TODO: check if an equivalent processing can't be done more quickly with a 514 // compiled regexp. 515 // Filter by first letter 516 final int firstCodePoint = text.codePointAt(0); 517 // Filter out words that don't start with a letter or an apostrophe 518 if (!isLetterCheckableByLanguage(firstCodePoint, script) 519 && '\'' != firstCodePoint) return true; 520 521 // Filter contents 522 final int length = text.length(); 523 int letterCount = 0; 524 for (int i = 0; i < length; ++i) { 525 final int codePoint = text.codePointAt(i); 526 // Any word containing a '@' is probably an e-mail address 527 // Any word containing a '/' is probably either an ad-hoc combination of two 528 // words or a URI - in either case we don't want to spell check that 529 if ('@' == codePoint 530 || '/' == codePoint) return true; 531 if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount; 532 } 533 // Guestimate heuristic: perform spell checking if at least 3/4 of the characters 534 // in this word are letters 535 return (letterCount * 4 < length * 3); 536 } 537 538 // Note : this must be reentrant 539 /** 540 * Gets a list of suggestions for a specific string. This returns a list of possible 541 * corrections for the text passed as an argument. It may split or group words, and 542 * even perform grammatical analysis. 543 */ 544 @Override 545 public SuggestionsInfo onGetSuggestions(final TextInfo textInfo, 546 final int suggestionsLimit) { 547 try { 548 final String text = textInfo.getText(); 549 550 if (shouldFilterOut(text, mScript)) { 551 DictAndProximity dictInfo = null; 552 try { 553 dictInfo = mDictionaryPool.takeOrGetNull(); 554 if (null == dictInfo) return getNotInDictEmptySuggestions(); 555 return dictInfo.mDictionary.isValidWord(text) ? getInDictEmptySuggestions() 556 : getNotInDictEmptySuggestions(); 557 } finally { 558 if (null != dictInfo) { 559 if (!mDictionaryPool.offer(dictInfo)) { 560 Log.e(TAG, "Can't re-insert a dictionary into its pool"); 561 } 562 } 563 } 564 } 565 566 // TODO: Don't gather suggestions if the limit is <= 0 unless necessary 567 final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text, 568 mService.mSuggestionThreshold, mService.mRecommendedThreshold, 569 suggestionsLimit); 570 final WordComposer composer = new WordComposer(); 571 final int length = text.length(); 572 for (int i = 0; i < length; ++i) { 573 final int character = text.codePointAt(i); 574 final int proximityIndex = 575 SpellCheckerProximityInfo.getIndexOfCodeForScript(character, mScript); 576 final int[] proximities; 577 if (-1 == proximityIndex) { 578 proximities = new int[] { character }; 579 } else { 580 // TODO: an initial examination seems to reveal this is actually used 581 // read-only. It should be possible to compute the arrays statically once 582 // and skip doing a copy each time here. 583 proximities = Arrays.copyOfRange( 584 SpellCheckerProximityInfo.getProximityForScript(mScript), 585 proximityIndex, 586 proximityIndex + SpellCheckerProximityInfo.ROW_SIZE); 587 } 588 composer.add(character, proximities, 589 WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); 590 } 591 592 final int capitalizeType = getCapitalizationType(text); 593 boolean isInDict = true; 594 DictAndProximity dictInfo = null; 595 try { 596 dictInfo = mDictionaryPool.takeOrGetNull(); 597 if (null == dictInfo) return getNotInDictEmptySuggestions(); 598 dictInfo.mDictionary.getWords(composer, suggestionsGatherer, 599 dictInfo.mProximityInfo); 600 isInDict = dictInfo.mDictionary.isValidWord(text); 601 if (!isInDict && CAPITALIZE_NONE != capitalizeType) { 602 // We want to test the word again if it's all caps or first caps only. 603 // If it's fully down, we already tested it, if it's mixed case, we don't 604 // want to test a lowercase version of it. 605 isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale)); 606 } 607 } finally { 608 if (null != dictInfo) { 609 if (!mDictionaryPool.offer(dictInfo)) { 610 Log.e(TAG, "Can't re-insert a dictionary into its pool"); 611 } 612 } 613 } 614 615 final SuggestionsGatherer.Result result = suggestionsGatherer.getResults( 616 capitalizeType, mLocale); 617 618 if (DBG) { 619 Log.i(TAG, "Spell checking results for " + text + " with suggestion limit " 620 + suggestionsLimit); 621 Log.i(TAG, "IsInDict = " + isInDict); 622 Log.i(TAG, "LooksLikeTypo = " + (!isInDict)); 623 Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions); 624 if (null != result.mSuggestions) { 625 for (String suggestion : result.mSuggestions) { 626 Log.i(TAG, suggestion); 627 } 628 } 629 } 630 631 final int flags = 632 (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY 633 : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) 634 | (result.mHasRecommendedSuggestions 635 ? SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS 636 : 0); 637 return new SuggestionsInfo(flags, result.mSuggestions); 638 } catch (RuntimeException e) { 639 // Don't kill the keyboard if there is a bug in the spell checker 640 if (DBG) { 641 throw e; 642 } else { 643 Log.e(TAG, "Exception while spellcheking: " + e); 644 return getNotInDictEmptySuggestions(); 645 } 646 } 647 } 648 } 649} 650