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