1 /* 2 * Copyright (C) 2011 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 android.widget; 18 19 import android.content.Context; 20 import android.text.Editable; 21 import android.text.Selection; 22 import android.text.Spanned; 23 import android.text.TextUtils; 24 import android.text.method.WordIterator; 25 import android.text.style.SpellCheckSpan; 26 import android.text.style.SuggestionSpan; 27 import android.util.Log; 28 import android.util.LruCache; 29 import android.view.textservice.SentenceSuggestionsInfo; 30 import android.view.textservice.SpellCheckerSession; 31 import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; 32 import android.view.textservice.SuggestionsInfo; 33 import android.view.textservice.TextInfo; 34 import android.view.textservice.TextServicesManager; 35 36 import com.android.internal.util.ArrayUtils; 37 import com.android.internal.util.GrowingArrayUtils; 38 39 import java.text.BreakIterator; 40 import java.util.Locale; 41 42 43 /** 44 * Helper class for TextView. Bridge between the TextView and the Dictionary service. 45 * 46 * @hide 47 */ 48 public class SpellChecker implements SpellCheckerSessionListener { 49 private static final String TAG = SpellChecker.class.getSimpleName(); 50 private static final boolean DBG = false; 51 52 // No more than this number of words will be parsed on each iteration to ensure a minimum 53 // lock of the UI thread 54 public static final int MAX_NUMBER_OF_WORDS = 50; 55 56 // Rough estimate, such that the word iterator interval usually does not need to be shifted 57 public static final int AVERAGE_WORD_LENGTH = 7; 58 59 // When parsing, use a character window of that size. Will be shifted if needed 60 public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS; 61 62 // Pause between each spell check to keep the UI smooth 63 private final static int SPELL_PAUSE_DURATION = 400; // milliseconds 64 65 private static final int MIN_SENTENCE_LENGTH = 50; 66 67 private static final int USE_SPAN_RANGE = -1; 68 69 private final TextView mTextView; 70 71 SpellCheckerSession mSpellCheckerSession; 72 // We assume that the sentence level spell check will always provide better results than words. 73 // Although word SC has a sequential option. 74 private boolean mIsSentenceSpellCheckSupported; 75 final int mCookie; 76 77 // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated 78 // SpellCheckSpan has been recycled and can be-reused. 79 // Contains null SpellCheckSpans after index mLength. 80 private int[] mIds; 81 private SpellCheckSpan[] mSpellCheckSpans; 82 // The mLength first elements of the above arrays have been initialized 83 private int mLength; 84 85 // Parsers on chunk of text, cutting text into words that will be checked 86 private SpellParser[] mSpellParsers = new SpellParser[0]; 87 88 private int mSpanSequenceCounter = 0; 89 90 private Locale mCurrentLocale; 91 92 // Shared by all SpellParsers. Cannot be shared with TextView since it may be used 93 // concurrently due to the asynchronous nature of onGetSuggestions. 94 private WordIterator mWordIterator; 95 96 private TextServicesManager mTextServicesManager; 97 98 private Runnable mSpellRunnable; 99 100 private static final int SUGGESTION_SPAN_CACHE_SIZE = 10; 101 private final LruCache<Long, SuggestionSpan> mSuggestionSpanCache = 102 new LruCache<Long, SuggestionSpan>(SUGGESTION_SPAN_CACHE_SIZE); 103 SpellChecker(TextView textView)104 public SpellChecker(TextView textView) { 105 mTextView = textView; 106 107 // Arbitrary: these arrays will automatically double their sizes on demand 108 final int size = 1; 109 mIds = ArrayUtils.newUnpaddedIntArray(size); 110 mSpellCheckSpans = new SpellCheckSpan[mIds.length]; 111 112 setLocale(mTextView.getSpellCheckerLocale()); 113 114 mCookie = hashCode(); 115 } 116 resetSession()117 private void resetSession() { 118 closeSession(); 119 120 mTextServicesManager = (TextServicesManager) mTextView.getContext(). 121 getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); 122 if (!mTextServicesManager.isSpellCheckerEnabled() 123 || mCurrentLocale == null 124 || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) { 125 mSpellCheckerSession = null; 126 } else { 127 mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession( 128 null /* Bundle not currently used by the textServicesManager */, 129 mCurrentLocale, this, 130 false /* means any available languages from current spell checker */); 131 mIsSentenceSpellCheckSupported = true; 132 } 133 134 // Restore SpellCheckSpans in pool 135 for (int i = 0; i < mLength; i++) { 136 mIds[i] = -1; 137 } 138 mLength = 0; 139 140 // Remove existing misspelled SuggestionSpans 141 mTextView.removeMisspelledSpans((Editable) mTextView.getText()); 142 mSuggestionSpanCache.evictAll(); 143 } 144 setLocale(Locale locale)145 private void setLocale(Locale locale) { 146 mCurrentLocale = locale; 147 148 resetSession(); 149 150 if (locale != null) { 151 // Change SpellParsers' wordIterator locale 152 mWordIterator = new WordIterator(locale); 153 } 154 155 // This class is the listener for locale change: warn other locale-aware objects 156 mTextView.onLocaleChanged(); 157 } 158 159 /** 160 * @return true if a spell checker session has successfully been created. Returns false if not, 161 * for instance when spell checking has been disabled in settings. 162 */ isSessionActive()163 private boolean isSessionActive() { 164 return mSpellCheckerSession != null; 165 } 166 closeSession()167 public void closeSession() { 168 if (mSpellCheckerSession != null) { 169 mSpellCheckerSession.close(); 170 } 171 172 final int length = mSpellParsers.length; 173 for (int i = 0; i < length; i++) { 174 mSpellParsers[i].stop(); 175 } 176 177 if (mSpellRunnable != null) { 178 mTextView.removeCallbacks(mSpellRunnable); 179 } 180 } 181 nextSpellCheckSpanIndex()182 private int nextSpellCheckSpanIndex() { 183 for (int i = 0; i < mLength; i++) { 184 if (mIds[i] < 0) return i; 185 } 186 187 mIds = GrowingArrayUtils.append(mIds, mLength, 0); 188 mSpellCheckSpans = GrowingArrayUtils.append( 189 mSpellCheckSpans, mLength, new SpellCheckSpan()); 190 mLength++; 191 return mLength - 1; 192 } 193 addSpellCheckSpan(Editable editable, int start, int end)194 private void addSpellCheckSpan(Editable editable, int start, int end) { 195 final int index = nextSpellCheckSpanIndex(); 196 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[index]; 197 editable.setSpan(spellCheckSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 198 spellCheckSpan.setSpellCheckInProgress(false); 199 mIds[index] = mSpanSequenceCounter++; 200 } 201 onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan)202 public void onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan) { 203 // Recycle any removed SpellCheckSpan (from this code or during text edition) 204 for (int i = 0; i < mLength; i++) { 205 if (mSpellCheckSpans[i] == spellCheckSpan) { 206 mIds[i] = -1; 207 return; 208 } 209 } 210 } 211 onSelectionChanged()212 public void onSelectionChanged() { 213 spellCheck(); 214 } 215 spellCheck(int start, int end)216 public void spellCheck(int start, int end) { 217 if (DBG) { 218 Log.d(TAG, "Start spell-checking: " + start + ", " + end); 219 } 220 final Locale locale = mTextView.getSpellCheckerLocale(); 221 final boolean isSessionActive = isSessionActive(); 222 if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) { 223 setLocale(locale); 224 // Re-check the entire text 225 start = 0; 226 end = mTextView.getText().length(); 227 } else { 228 final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled(); 229 if (isSessionActive != spellCheckerActivated) { 230 // Spell checker has been turned of or off since last spellCheck 231 resetSession(); 232 } 233 } 234 235 if (!isSessionActive) return; 236 237 // Find first available SpellParser from pool 238 final int length = mSpellParsers.length; 239 for (int i = 0; i < length; i++) { 240 final SpellParser spellParser = mSpellParsers[i]; 241 if (spellParser.isFinished()) { 242 spellParser.parse(start, end); 243 return; 244 } 245 } 246 247 if (DBG) { 248 Log.d(TAG, "new spell parser."); 249 } 250 // No available parser found in pool, create a new one 251 SpellParser[] newSpellParsers = new SpellParser[length + 1]; 252 System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length); 253 mSpellParsers = newSpellParsers; 254 255 SpellParser spellParser = new SpellParser(); 256 mSpellParsers[length] = spellParser; 257 spellParser.parse(start, end); 258 } 259 spellCheck()260 private void spellCheck() { 261 if (mSpellCheckerSession == null) return; 262 263 Editable editable = (Editable) mTextView.getText(); 264 final int selectionStart = Selection.getSelectionStart(editable); 265 final int selectionEnd = Selection.getSelectionEnd(editable); 266 267 TextInfo[] textInfos = new TextInfo[mLength]; 268 int textInfosCount = 0; 269 270 for (int i = 0; i < mLength; i++) { 271 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; 272 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue; 273 274 final int start = editable.getSpanStart(spellCheckSpan); 275 final int end = editable.getSpanEnd(spellCheckSpan); 276 277 // Do not check this word if the user is currently editing it 278 final boolean isEditing; 279 280 // Defer spell check when typing a word with an interior apostrophe. 281 // TODO: a better solution to this would be to make the word 282 // iterator locale-sensitive and include the apostrophe in 283 // languages that use it (such as English). 284 final boolean apostrophe = (selectionStart == end + 1 && editable.charAt(end) == '\''); 285 if (mIsSentenceSpellCheckSupported) { 286 // Allow the overlap of the cursor and the first boundary of the spell check span 287 // no to skip the spell check of the following word because the 288 // following word will never be spell-checked even if the user finishes composing 289 isEditing = !apostrophe && (selectionEnd <= start || selectionStart > end); 290 } else { 291 isEditing = !apostrophe && (selectionEnd < start || selectionStart > end); 292 } 293 if (start >= 0 && end > start && isEditing) { 294 spellCheckSpan.setSpellCheckInProgress(true); 295 final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]); 296 textInfos[textInfosCount++] = textInfo; 297 if (DBG) { 298 Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ") text = " 299 + textInfo.getSequence() + ", cookie = " + mCookie + ", seq = " 300 + mIds[i] + ", sel start = " + selectionStart + ", sel end = " 301 + selectionEnd + ", start = " + start + ", end = " + end); 302 } 303 } 304 } 305 306 if (textInfosCount > 0) { 307 if (textInfosCount < textInfos.length) { 308 TextInfo[] textInfosCopy = new TextInfo[textInfosCount]; 309 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount); 310 textInfos = textInfosCopy; 311 } 312 313 if (mIsSentenceSpellCheckSupported) { 314 mSpellCheckerSession.getSentenceSuggestions( 315 textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE); 316 } else { 317 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE, 318 false /* TODO Set sequentialWords to true for initial spell check */); 319 } 320 } 321 } 322 onGetSuggestionsInternal( SuggestionsInfo suggestionsInfo, int offset, int length)323 private SpellCheckSpan onGetSuggestionsInternal( 324 SuggestionsInfo suggestionsInfo, int offset, int length) { 325 if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) { 326 return null; 327 } 328 final Editable editable = (Editable) mTextView.getText(); 329 final int sequenceNumber = suggestionsInfo.getSequence(); 330 for (int k = 0; k < mLength; ++k) { 331 if (sequenceNumber == mIds[k]) { 332 final int attributes = suggestionsInfo.getSuggestionsAttributes(); 333 final boolean isInDictionary = 334 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0); 335 final boolean looksLikeTypo = 336 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); 337 338 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k]; 339 //TODO: we need to change that rule for results from a sentence-level spell 340 // checker that will probably be in dictionary. 341 if (!isInDictionary && looksLikeTypo) { 342 createMisspelledSuggestionSpan( 343 editable, suggestionsInfo, spellCheckSpan, offset, length); 344 } else { 345 // Valid word -- isInDictionary || !looksLikeTypo 346 if (mIsSentenceSpellCheckSupported) { 347 // Allow the spell checker to remove existing misspelled span by 348 // overwriting the span over the same place 349 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan); 350 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan); 351 final int start; 352 final int end; 353 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) { 354 start = spellCheckSpanStart + offset; 355 end = start + length; 356 } else { 357 start = spellCheckSpanStart; 358 end = spellCheckSpanEnd; 359 } 360 if (spellCheckSpanStart >= 0 && spellCheckSpanEnd > spellCheckSpanStart 361 && end > start) { 362 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end)); 363 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key); 364 if (tempSuggestionSpan != null) { 365 if (DBG) { 366 Log.i(TAG, "Remove existing misspelled span. " 367 + editable.subSequence(start, end)); 368 } 369 editable.removeSpan(tempSuggestionSpan); 370 mSuggestionSpanCache.remove(key); 371 } 372 } 373 } 374 } 375 return spellCheckSpan; 376 } 377 } 378 return null; 379 } 380 381 @Override onGetSuggestions(SuggestionsInfo[] results)382 public void onGetSuggestions(SuggestionsInfo[] results) { 383 final Editable editable = (Editable) mTextView.getText(); 384 for (int i = 0; i < results.length; ++i) { 385 final SpellCheckSpan spellCheckSpan = 386 onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE); 387 if (spellCheckSpan != null) { 388 // onSpellCheckSpanRemoved will recycle this span in the pool 389 editable.removeSpan(spellCheckSpan); 390 } 391 } 392 scheduleNewSpellCheck(); 393 } 394 395 @Override onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)396 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { 397 final Editable editable = (Editable) mTextView.getText(); 398 399 for (int i = 0; i < results.length; ++i) { 400 final SentenceSuggestionsInfo ssi = results[i]; 401 if (ssi == null) { 402 continue; 403 } 404 SpellCheckSpan spellCheckSpan = null; 405 for (int j = 0; j < ssi.getSuggestionsCount(); ++j) { 406 final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j); 407 if (suggestionsInfo == null) { 408 continue; 409 } 410 final int offset = ssi.getOffsetAt(j); 411 final int length = ssi.getLengthAt(j); 412 final SpellCheckSpan scs = onGetSuggestionsInternal( 413 suggestionsInfo, offset, length); 414 if (spellCheckSpan == null && scs != null) { 415 // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same 416 // SentenceSuggestionsInfo. Removal is deferred after this loop. 417 spellCheckSpan = scs; 418 } 419 } 420 if (spellCheckSpan != null) { 421 // onSpellCheckSpanRemoved will recycle this span in the pool 422 editable.removeSpan(spellCheckSpan); 423 } 424 } 425 scheduleNewSpellCheck(); 426 } 427 scheduleNewSpellCheck()428 private void scheduleNewSpellCheck() { 429 if (DBG) { 430 Log.i(TAG, "schedule new spell check."); 431 } 432 if (mSpellRunnable == null) { 433 mSpellRunnable = new Runnable() { 434 @Override 435 public void run() { 436 final int length = mSpellParsers.length; 437 for (int i = 0; i < length; i++) { 438 final SpellParser spellParser = mSpellParsers[i]; 439 if (!spellParser.isFinished()) { 440 spellParser.parse(); 441 break; // run one spell parser at a time to bound running time 442 } 443 } 444 } 445 }; 446 } else { 447 mTextView.removeCallbacks(mSpellRunnable); 448 } 449 450 mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION); 451 } 452 createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo, SpellCheckSpan spellCheckSpan, int offset, int length)453 private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo, 454 SpellCheckSpan spellCheckSpan, int offset, int length) { 455 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan); 456 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan); 457 if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart) 458 return; // span was removed in the meantime 459 460 final int start; 461 final int end; 462 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) { 463 start = spellCheckSpanStart + offset; 464 end = start + length; 465 } else { 466 start = spellCheckSpanStart; 467 end = spellCheckSpanEnd; 468 } 469 470 final int suggestionsCount = suggestionsInfo.getSuggestionsCount(); 471 String[] suggestions; 472 if (suggestionsCount > 0) { 473 suggestions = new String[suggestionsCount]; 474 for (int i = 0; i < suggestionsCount; i++) { 475 suggestions[i] = suggestionsInfo.getSuggestionAt(i); 476 } 477 } else { 478 suggestions = ArrayUtils.emptyArray(String.class); 479 } 480 481 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions, 482 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED); 483 // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface 484 // to share the logic of word level spell checker and sentence level spell checker 485 if (mIsSentenceSpellCheckSupported) { 486 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end)); 487 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key); 488 if (tempSuggestionSpan != null) { 489 if (DBG) { 490 Log.i(TAG, "Cached span on the same position is cleard. " 491 + editable.subSequence(start, end)); 492 } 493 editable.removeSpan(tempSuggestionSpan); 494 } 495 mSuggestionSpanCache.put(key, suggestionSpan); 496 } 497 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 498 499 mTextView.invalidateRegion(start, end, false /* No cursor involved */); 500 } 501 502 private class SpellParser { 503 private Object mRange = new Object(); 504 parse(int start, int end)505 public void parse(int start, int end) { 506 final int max = mTextView.length(); 507 final int parseEnd; 508 if (end > max) { 509 Log.w(TAG, "Parse invalid region, from " + start + " to " + end); 510 parseEnd = max; 511 } else { 512 parseEnd = end; 513 } 514 if (parseEnd > start) { 515 setRangeSpan((Editable) mTextView.getText(), start, parseEnd); 516 parse(); 517 } 518 } 519 isFinished()520 public boolean isFinished() { 521 return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0; 522 } 523 stop()524 public void stop() { 525 removeRangeSpan((Editable) mTextView.getText()); 526 } 527 setRangeSpan(Editable editable, int start, int end)528 private void setRangeSpan(Editable editable, int start, int end) { 529 if (DBG) { 530 Log.d(TAG, "set next range span: " + start + ", " + end); 531 } 532 editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 533 } 534 removeRangeSpan(Editable editable)535 private void removeRangeSpan(Editable editable) { 536 if (DBG) { 537 Log.d(TAG, "Remove range span." + editable.getSpanStart(editable) 538 + editable.getSpanEnd(editable)); 539 } 540 editable.removeSpan(mRange); 541 } 542 parse()543 public void parse() { 544 Editable editable = (Editable) mTextView.getText(); 545 // Iterate over the newly added text and schedule new SpellCheckSpans 546 final int start; 547 if (mIsSentenceSpellCheckSupported) { 548 // TODO: Find the start position of the sentence. 549 // Set span with the context 550 start = Math.max( 551 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH); 552 } else { 553 start = editable.getSpanStart(mRange); 554 } 555 556 final int end = editable.getSpanEnd(mRange); 557 558 int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL); 559 mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd); 560 561 // Move back to the beginning of the current word, if any 562 int wordStart = mWordIterator.preceding(start); 563 int wordEnd; 564 if (wordStart == BreakIterator.DONE) { 565 wordEnd = mWordIterator.following(start); 566 if (wordEnd != BreakIterator.DONE) { 567 wordStart = mWordIterator.getBeginning(wordEnd); 568 } 569 } else { 570 wordEnd = mWordIterator.getEnd(wordStart); 571 } 572 if (wordEnd == BreakIterator.DONE) { 573 if (DBG) { 574 Log.i(TAG, "No more spell check."); 575 } 576 removeRangeSpan(editable); 577 return; 578 } 579 580 // We need to expand by one character because we want to include the spans that 581 // end/start at position start/end respectively. 582 SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1, 583 SpellCheckSpan.class); 584 SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1, 585 SuggestionSpan.class); 586 587 int wordCount = 0; 588 boolean scheduleOtherSpellCheck = false; 589 590 if (mIsSentenceSpellCheckSupported) { 591 if (wordIteratorWindowEnd < end) { 592 if (DBG) { 593 Log.i(TAG, "schedule other spell check."); 594 } 595 // Several batches needed on that region. Cut after last previous word 596 scheduleOtherSpellCheck = true; 597 } 598 int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd); 599 boolean correct = spellCheckEnd != BreakIterator.DONE; 600 if (correct) { 601 spellCheckEnd = mWordIterator.getEnd(spellCheckEnd); 602 correct = spellCheckEnd != BreakIterator.DONE; 603 } 604 if (!correct) { 605 if (DBG) { 606 Log.i(TAG, "Incorrect range span."); 607 } 608 removeRangeSpan(editable); 609 return; 610 } 611 do { 612 // TODO: Find the start position of the sentence. 613 int spellCheckStart = wordStart; 614 boolean createSpellCheckSpan = true; 615 // Cancel or merge overlapped spell check spans 616 for (int i = 0; i < mLength; ++i) { 617 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; 618 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) { 619 continue; 620 } 621 final int spanStart = editable.getSpanStart(spellCheckSpan); 622 final int spanEnd = editable.getSpanEnd(spellCheckSpan); 623 if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) { 624 // No need to merge 625 continue; 626 } 627 if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) { 628 // There is a completely overlapped spell check span 629 // skip this span 630 createSpellCheckSpan = false; 631 if (DBG) { 632 Log.i(TAG, "The range is overrapped. Skip spell check."); 633 } 634 break; 635 } 636 // This spellCheckSpan is replaced by the one we are creating 637 editable.removeSpan(spellCheckSpan); 638 spellCheckStart = Math.min(spanStart, spellCheckStart); 639 spellCheckEnd = Math.max(spanEnd, spellCheckEnd); 640 } 641 642 if (DBG) { 643 Log.d(TAG, "addSpellCheckSpan: " 644 + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart 645 + ", next = " + scheduleOtherSpellCheck + "\n" 646 + editable.subSequence(spellCheckStart, spellCheckEnd)); 647 } 648 649 // Stop spell checking when there are no characters in the range. 650 if (spellCheckEnd < start) { 651 break; 652 } 653 if (spellCheckEnd <= spellCheckStart) { 654 Log.w(TAG, "Trying to spellcheck invalid region, from " 655 + start + " to " + end); 656 break; 657 } 658 if (createSpellCheckSpan) { 659 addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd); 660 } 661 } while (false); 662 wordStart = spellCheckEnd; 663 } else { 664 while (wordStart <= end) { 665 if (wordEnd >= start && wordEnd > wordStart) { 666 if (wordCount >= MAX_NUMBER_OF_WORDS) { 667 scheduleOtherSpellCheck = true; 668 break; 669 } 670 // A new word has been created across the interval boundaries with this 671 // edit. The previous spans (that ended on start / started on end) are 672 // not valid anymore and must be removed. 673 if (wordStart < start && wordEnd > start) { 674 removeSpansAt(editable, start, spellCheckSpans); 675 removeSpansAt(editable, start, suggestionSpans); 676 } 677 678 if (wordStart < end && wordEnd > end) { 679 removeSpansAt(editable, end, spellCheckSpans); 680 removeSpansAt(editable, end, suggestionSpans); 681 } 682 683 // Do not create new boundary spans if they already exist 684 boolean createSpellCheckSpan = true; 685 if (wordEnd == start) { 686 for (int i = 0; i < spellCheckSpans.length; i++) { 687 final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]); 688 if (spanEnd == start) { 689 createSpellCheckSpan = false; 690 break; 691 } 692 } 693 } 694 695 if (wordStart == end) { 696 for (int i = 0; i < spellCheckSpans.length; i++) { 697 final int spanStart = editable.getSpanStart(spellCheckSpans[i]); 698 if (spanStart == end) { 699 createSpellCheckSpan = false; 700 break; 701 } 702 } 703 } 704 705 if (createSpellCheckSpan) { 706 addSpellCheckSpan(editable, wordStart, wordEnd); 707 } 708 wordCount++; 709 } 710 711 // iterate word by word 712 int originalWordEnd = wordEnd; 713 wordEnd = mWordIterator.following(wordEnd); 714 if ((wordIteratorWindowEnd < end) && 715 (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) { 716 wordIteratorWindowEnd = 717 Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL); 718 mWordIterator.setCharSequence( 719 editable, originalWordEnd, wordIteratorWindowEnd); 720 wordEnd = mWordIterator.following(originalWordEnd); 721 } 722 if (wordEnd == BreakIterator.DONE) break; 723 wordStart = mWordIterator.getBeginning(wordEnd); 724 if (wordStart == BreakIterator.DONE) { 725 break; 726 } 727 } 728 } 729 730 if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) { 731 // Update range span: start new spell check from last wordStart 732 setRangeSpan(editable, wordStart, end); 733 } else { 734 removeRangeSpan(editable); 735 } 736 737 spellCheck(); 738 } 739 removeSpansAt(Editable editable, int offset, T[] spans)740 private <T> void removeSpansAt(Editable editable, int offset, T[] spans) { 741 final int length = spans.length; 742 for (int i = 0; i < length; i++) { 743 final T span = spans[i]; 744 final int start = editable.getSpanStart(span); 745 if (start > offset) continue; 746 final int end = editable.getSpanEnd(span); 747 if (end < offset) continue; 748 editable.removeSpan(span); 749 } 750 } 751 } 752 haveWordBoundariesChanged(final Editable editable, final int start, final int end, final int spanStart, final int spanEnd)753 public static boolean haveWordBoundariesChanged(final Editable editable, final int start, 754 final int end, final int spanStart, final int spanEnd) { 755 final boolean haveWordBoundariesChanged; 756 if (spanEnd != start && spanStart != end) { 757 haveWordBoundariesChanged = true; 758 if (DBG) { 759 Log.d(TAG, "(1) Text inside the span has been modified. Remove."); 760 } 761 } else if (spanEnd == start && start < editable.length()) { 762 final int codePoint = Character.codePointAt(editable, start); 763 haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint); 764 if (DBG) { 765 Log.d(TAG, "(2) Characters have been appended to the spanned text. " 766 + (haveWordBoundariesChanged ? "Remove.<" : "Keep. <") + (char)(codePoint) 767 + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", " 768 + start); 769 } 770 } else if (spanStart == end && end > 0) { 771 final int codePoint = Character.codePointBefore(editable, end); 772 haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint); 773 if (DBG) { 774 Log.d(TAG, "(3) Characters have been prepended to the spanned text. " 775 + (haveWordBoundariesChanged ? "Remove.<" : "Keep.<") + (char)(codePoint) 776 + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", " 777 + end); 778 } 779 } else { 780 if (DBG) { 781 Log.d(TAG, "(4) Characters adjacent to the spanned text were deleted. Keep."); 782 } 783 haveWordBoundariesChanged = false; 784 } 785 return haveWordBoundariesChanged; 786 } 787 } 788