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