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 ending with a punctuation like an apostrophe 281 // which could end up being a mid-word punctuation. 282 if (selectionStart == end + 1 283 && WordIterator.isMidWordPunctuation( 284 mCurrentLocale, Character.codePointBefore(editable, end + 1))) { 285 isEditing = false; 286 } else if (mIsSentenceSpellCheckSupported) { 287 // Allow the overlap of the cursor and the first boundary of the spell check span 288 // no to skip the spell check of the following word because the 289 // following word will never be spell-checked even if the user finishes composing 290 isEditing = selectionEnd <= start || selectionStart > end; 291 } else { 292 isEditing = selectionEnd < start || selectionStart > end; 293 } 294 if (start >= 0 && end > start && isEditing) { 295 spellCheckSpan.setSpellCheckInProgress(true); 296 final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]); 297 textInfos[textInfosCount++] = textInfo; 298 if (DBG) { 299 Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ") text = " 300 + textInfo.getSequence() + ", cookie = " + mCookie + ", seq = " 301 + mIds[i] + ", sel start = " + selectionStart + ", sel end = " 302 + selectionEnd + ", start = " + start + ", end = " + end); 303 } 304 } 305 } 306 307 if (textInfosCount > 0) { 308 if (textInfosCount < textInfos.length) { 309 TextInfo[] textInfosCopy = new TextInfo[textInfosCount]; 310 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount); 311 textInfos = textInfosCopy; 312 } 313 314 if (mIsSentenceSpellCheckSupported) { 315 mSpellCheckerSession.getSentenceSuggestions( 316 textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE); 317 } else { 318 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE, 319 false /* TODO Set sequentialWords to true for initial spell check */); 320 } 321 } 322 } 323 onGetSuggestionsInternal( SuggestionsInfo suggestionsInfo, int offset, int length)324 private SpellCheckSpan onGetSuggestionsInternal( 325 SuggestionsInfo suggestionsInfo, int offset, int length) { 326 if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) { 327 return null; 328 } 329 final Editable editable = (Editable) mTextView.getText(); 330 final int sequenceNumber = suggestionsInfo.getSequence(); 331 for (int k = 0; k < mLength; ++k) { 332 if (sequenceNumber == mIds[k]) { 333 final int attributes = suggestionsInfo.getSuggestionsAttributes(); 334 final boolean isInDictionary = 335 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0); 336 final boolean looksLikeTypo = 337 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); 338 339 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k]; 340 //TODO: we need to change that rule for results from a sentence-level spell 341 // checker that will probably be in dictionary. 342 if (!isInDictionary && looksLikeTypo) { 343 createMisspelledSuggestionSpan( 344 editable, suggestionsInfo, spellCheckSpan, offset, length); 345 } else { 346 // Valid word -- isInDictionary || !looksLikeTypo 347 if (mIsSentenceSpellCheckSupported) { 348 // Allow the spell checker to remove existing misspelled span by 349 // overwriting the span over the same place 350 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan); 351 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan); 352 final int start; 353 final int end; 354 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) { 355 start = spellCheckSpanStart + offset; 356 end = start + length; 357 } else { 358 start = spellCheckSpanStart; 359 end = spellCheckSpanEnd; 360 } 361 if (spellCheckSpanStart >= 0 && spellCheckSpanEnd > spellCheckSpanStart 362 && end > start) { 363 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end)); 364 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key); 365 if (tempSuggestionSpan != null) { 366 if (DBG) { 367 Log.i(TAG, "Remove existing misspelled span. " 368 + editable.subSequence(start, end)); 369 } 370 editable.removeSpan(tempSuggestionSpan); 371 mSuggestionSpanCache.remove(key); 372 } 373 } 374 } 375 } 376 return spellCheckSpan; 377 } 378 } 379 return null; 380 } 381 382 @Override onGetSuggestions(SuggestionsInfo[] results)383 public void onGetSuggestions(SuggestionsInfo[] results) { 384 final Editable editable = (Editable) mTextView.getText(); 385 for (int i = 0; i < results.length; ++i) { 386 final SpellCheckSpan spellCheckSpan = 387 onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE); 388 if (spellCheckSpan != null) { 389 // onSpellCheckSpanRemoved will recycle this span in the pool 390 editable.removeSpan(spellCheckSpan); 391 } 392 } 393 scheduleNewSpellCheck(); 394 } 395 396 @Override onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)397 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { 398 final Editable editable = (Editable) mTextView.getText(); 399 400 for (int i = 0; i < results.length; ++i) { 401 final SentenceSuggestionsInfo ssi = results[i]; 402 if (ssi == null) { 403 continue; 404 } 405 SpellCheckSpan spellCheckSpan = null; 406 for (int j = 0; j < ssi.getSuggestionsCount(); ++j) { 407 final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j); 408 if (suggestionsInfo == null) { 409 continue; 410 } 411 final int offset = ssi.getOffsetAt(j); 412 final int length = ssi.getLengthAt(j); 413 final SpellCheckSpan scs = onGetSuggestionsInternal( 414 suggestionsInfo, offset, length); 415 if (spellCheckSpan == null && scs != null) { 416 // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same 417 // SentenceSuggestionsInfo. Removal is deferred after this loop. 418 spellCheckSpan = scs; 419 } 420 } 421 if (spellCheckSpan != null) { 422 // onSpellCheckSpanRemoved will recycle this span in the pool 423 editable.removeSpan(spellCheckSpan); 424 } 425 } 426 scheduleNewSpellCheck(); 427 } 428 scheduleNewSpellCheck()429 private void scheduleNewSpellCheck() { 430 if (DBG) { 431 Log.i(TAG, "schedule new spell check."); 432 } 433 if (mSpellRunnable == null) { 434 mSpellRunnable = new Runnable() { 435 @Override 436 public void run() { 437 final int length = mSpellParsers.length; 438 for (int i = 0; i < length; i++) { 439 final SpellParser spellParser = mSpellParsers[i]; 440 if (!spellParser.isFinished()) { 441 spellParser.parse(); 442 break; // run one spell parser at a time to bound running time 443 } 444 } 445 } 446 }; 447 } else { 448 mTextView.removeCallbacks(mSpellRunnable); 449 } 450 451 mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION); 452 } 453 createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo, SpellCheckSpan spellCheckSpan, int offset, int length)454 private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo, 455 SpellCheckSpan spellCheckSpan, int offset, int length) { 456 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan); 457 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan); 458 if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart) 459 return; // span was removed in the meantime 460 461 final int start; 462 final int end; 463 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) { 464 start = spellCheckSpanStart + offset; 465 end = start + length; 466 } else { 467 start = spellCheckSpanStart; 468 end = spellCheckSpanEnd; 469 } 470 471 final int suggestionsCount = suggestionsInfo.getSuggestionsCount(); 472 String[] suggestions; 473 if (suggestionsCount > 0) { 474 suggestions = new String[suggestionsCount]; 475 for (int i = 0; i < suggestionsCount; i++) { 476 suggestions[i] = suggestionsInfo.getSuggestionAt(i); 477 } 478 } else { 479 suggestions = ArrayUtils.emptyArray(String.class); 480 } 481 482 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions, 483 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED); 484 // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface 485 // to share the logic of word level spell checker and sentence level spell checker 486 if (mIsSentenceSpellCheckSupported) { 487 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end)); 488 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key); 489 if (tempSuggestionSpan != null) { 490 if (DBG) { 491 Log.i(TAG, "Cached span on the same position is cleard. " 492 + editable.subSequence(start, end)); 493 } 494 editable.removeSpan(tempSuggestionSpan); 495 } 496 mSuggestionSpanCache.put(key, suggestionSpan); 497 } 498 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 499 500 mTextView.invalidateRegion(start, end, false /* No cursor involved */); 501 } 502 503 private class SpellParser { 504 private Object mRange = new Object(); 505 parse(int start, int end)506 public void parse(int start, int end) { 507 final int max = mTextView.length(); 508 final int parseEnd; 509 if (end > max) { 510 Log.w(TAG, "Parse invalid region, from " + start + " to " + end); 511 parseEnd = max; 512 } else { 513 parseEnd = end; 514 } 515 if (parseEnd > start) { 516 setRangeSpan((Editable) mTextView.getText(), start, parseEnd); 517 parse(); 518 } 519 } 520 isFinished()521 public boolean isFinished() { 522 return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0; 523 } 524 stop()525 public void stop() { 526 removeRangeSpan((Editable) mTextView.getText()); 527 } 528 setRangeSpan(Editable editable, int start, int end)529 private void setRangeSpan(Editable editable, int start, int end) { 530 if (DBG) { 531 Log.d(TAG, "set next range span: " + start + ", " + end); 532 } 533 editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 534 } 535 removeRangeSpan(Editable editable)536 private void removeRangeSpan(Editable editable) { 537 if (DBG) { 538 Log.d(TAG, "Remove range span." + editable.getSpanStart(editable) 539 + editable.getSpanEnd(editable)); 540 } 541 editable.removeSpan(mRange); 542 } 543 parse()544 public void parse() { 545 Editable editable = (Editable) mTextView.getText(); 546 // Iterate over the newly added text and schedule new SpellCheckSpans 547 final int start; 548 if (mIsSentenceSpellCheckSupported) { 549 // TODO: Find the start position of the sentence. 550 // Set span with the context 551 start = Math.max( 552 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH); 553 } else { 554 start = editable.getSpanStart(mRange); 555 } 556 557 final int end = editable.getSpanEnd(mRange); 558 559 int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL); 560 mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd); 561 562 // Move back to the beginning of the current word, if any 563 int wordStart = mWordIterator.preceding(start); 564 int wordEnd; 565 if (wordStart == BreakIterator.DONE) { 566 wordEnd = mWordIterator.following(start); 567 if (wordEnd != BreakIterator.DONE) { 568 wordStart = mWordIterator.getBeginning(wordEnd); 569 } 570 } else { 571 wordEnd = mWordIterator.getEnd(wordStart); 572 } 573 if (wordEnd == BreakIterator.DONE) { 574 if (DBG) { 575 Log.i(TAG, "No more spell check."); 576 } 577 removeRangeSpan(editable); 578 return; 579 } 580 581 // We need to expand by one character because we want to include the spans that 582 // end/start at position start/end respectively. 583 SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1, 584 SpellCheckSpan.class); 585 SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1, 586 SuggestionSpan.class); 587 588 int wordCount = 0; 589 boolean scheduleOtherSpellCheck = false; 590 591 if (mIsSentenceSpellCheckSupported) { 592 if (wordIteratorWindowEnd < end) { 593 if (DBG) { 594 Log.i(TAG, "schedule other spell check."); 595 } 596 // Several batches needed on that region. Cut after last previous word 597 scheduleOtherSpellCheck = true; 598 } 599 int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd); 600 boolean correct = spellCheckEnd != BreakIterator.DONE; 601 if (correct) { 602 spellCheckEnd = mWordIterator.getEnd(spellCheckEnd); 603 correct = spellCheckEnd != BreakIterator.DONE; 604 } 605 if (!correct) { 606 if (DBG) { 607 Log.i(TAG, "Incorrect range span."); 608 } 609 removeRangeSpan(editable); 610 return; 611 } 612 do { 613 // TODO: Find the start position of the sentence. 614 int spellCheckStart = wordStart; 615 boolean createSpellCheckSpan = true; 616 // Cancel or merge overlapped spell check spans 617 for (int i = 0; i < mLength; ++i) { 618 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; 619 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) { 620 continue; 621 } 622 final int spanStart = editable.getSpanStart(spellCheckSpan); 623 final int spanEnd = editable.getSpanEnd(spellCheckSpan); 624 if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) { 625 // No need to merge 626 continue; 627 } 628 if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) { 629 // There is a completely overlapped spell check span 630 // skip this span 631 createSpellCheckSpan = false; 632 if (DBG) { 633 Log.i(TAG, "The range is overrapped. Skip spell check."); 634 } 635 break; 636 } 637 // This spellCheckSpan is replaced by the one we are creating 638 editable.removeSpan(spellCheckSpan); 639 spellCheckStart = Math.min(spanStart, spellCheckStart); 640 spellCheckEnd = Math.max(spanEnd, spellCheckEnd); 641 } 642 643 if (DBG) { 644 Log.d(TAG, "addSpellCheckSpan: " 645 + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart 646 + ", next = " + scheduleOtherSpellCheck + "\n" 647 + editable.subSequence(spellCheckStart, spellCheckEnd)); 648 } 649 650 // Stop spell checking when there are no characters in the range. 651 if (spellCheckEnd < start) { 652 break; 653 } 654 if (spellCheckEnd <= spellCheckStart) { 655 Log.w(TAG, "Trying to spellcheck invalid region, from " 656 + start + " to " + end); 657 break; 658 } 659 if (createSpellCheckSpan) { 660 addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd); 661 } 662 } while (false); 663 wordStart = spellCheckEnd; 664 } else { 665 while (wordStart <= end) { 666 if (wordEnd >= start && wordEnd > wordStart) { 667 if (wordCount >= MAX_NUMBER_OF_WORDS) { 668 scheduleOtherSpellCheck = true; 669 break; 670 } 671 // A new word has been created across the interval boundaries with this 672 // edit. The previous spans (that ended on start / started on end) are 673 // not valid anymore and must be removed. 674 if (wordStart < start && wordEnd > start) { 675 removeSpansAt(editable, start, spellCheckSpans); 676 removeSpansAt(editable, start, suggestionSpans); 677 } 678 679 if (wordStart < end && wordEnd > end) { 680 removeSpansAt(editable, end, spellCheckSpans); 681 removeSpansAt(editable, end, suggestionSpans); 682 } 683 684 // Do not create new boundary spans if they already exist 685 boolean createSpellCheckSpan = true; 686 if (wordEnd == start) { 687 for (int i = 0; i < spellCheckSpans.length; i++) { 688 final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]); 689 if (spanEnd == start) { 690 createSpellCheckSpan = false; 691 break; 692 } 693 } 694 } 695 696 if (wordStart == end) { 697 for (int i = 0; i < spellCheckSpans.length; i++) { 698 final int spanStart = editable.getSpanStart(spellCheckSpans[i]); 699 if (spanStart == end) { 700 createSpellCheckSpan = false; 701 break; 702 } 703 } 704 } 705 706 if (createSpellCheckSpan) { 707 addSpellCheckSpan(editable, wordStart, wordEnd); 708 } 709 wordCount++; 710 } 711 712 // iterate word by word 713 int originalWordEnd = wordEnd; 714 wordEnd = mWordIterator.following(wordEnd); 715 if ((wordIteratorWindowEnd < end) && 716 (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) { 717 wordIteratorWindowEnd = 718 Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL); 719 mWordIterator.setCharSequence( 720 editable, originalWordEnd, wordIteratorWindowEnd); 721 wordEnd = mWordIterator.following(originalWordEnd); 722 } 723 if (wordEnd == BreakIterator.DONE) break; 724 wordStart = mWordIterator.getBeginning(wordEnd); 725 if (wordStart == BreakIterator.DONE) { 726 break; 727 } 728 } 729 } 730 731 if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) { 732 // Update range span: start new spell check from last wordStart 733 setRangeSpan(editable, wordStart, end); 734 } else { 735 removeRangeSpan(editable); 736 } 737 738 spellCheck(); 739 } 740 removeSpansAt(Editable editable, int offset, T[] spans)741 private <T> void removeSpansAt(Editable editable, int offset, T[] spans) { 742 final int length = spans.length; 743 for (int i = 0; i < length; i++) { 744 final T span = spans[i]; 745 final int start = editable.getSpanStart(span); 746 if (start > offset) continue; 747 final int end = editable.getSpanEnd(span); 748 if (end < offset) continue; 749 editable.removeSpan(span); 750 } 751 } 752 } 753 haveWordBoundariesChanged(final Editable editable, final int start, final int end, final int spanStart, final int spanEnd)754 public static boolean haveWordBoundariesChanged(final Editable editable, final int start, 755 final int end, final int spanStart, final int spanEnd) { 756 final boolean haveWordBoundariesChanged; 757 if (spanEnd != start && spanStart != end) { 758 haveWordBoundariesChanged = true; 759 if (DBG) { 760 Log.d(TAG, "(1) Text inside the span has been modified. Remove."); 761 } 762 } else if (spanEnd == start && start < editable.length()) { 763 final int codePoint = Character.codePointAt(editable, start); 764 haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint); 765 if (DBG) { 766 Log.d(TAG, "(2) Characters have been appended to the spanned text. " 767 + (haveWordBoundariesChanged ? "Remove.<" : "Keep. <") + (char)(codePoint) 768 + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", " 769 + start); 770 } 771 } else if (spanStart == end && end > 0) { 772 final int codePoint = Character.codePointBefore(editable, end); 773 haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint); 774 if (DBG) { 775 Log.d(TAG, "(3) Characters have been prepended to the spanned text. " 776 + (haveWordBoundariesChanged ? "Remove.<" : "Keep.<") + (char)(codePoint) 777 + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", " 778 + end); 779 } 780 } else { 781 if (DBG) { 782 Log.d(TAG, "(4) Characters adjacent to the spanned text were deleted. Keep."); 783 } 784 haveWordBoundariesChanged = false; 785 } 786 return haveWordBoundariesChanged; 787 } 788 } 789