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.SpannableStringBuilder; 23 import android.text.Spanned; 24 import android.text.TextUtils; 25 import android.text.method.WordIterator; 26 import android.text.style.SpellCheckSpan; 27 import android.text.style.SuggestionSpan; 28 import android.util.Log; 29 import android.util.LruCache; 30 import android.view.textservice.SentenceSuggestionsInfo; 31 import android.view.textservice.SpellCheckerSession; 32 import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; 33 import android.view.textservice.SuggestionsInfo; 34 import android.view.textservice.TextInfo; 35 import android.view.textservice.TextServicesManager; 36 37 import com.android.internal.util.ArrayUtils; 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 Dictionnary 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 chunck 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 = ArrayUtils.idealObjectArraySize(1); 109 mIds = new int[size]; 110 mSpellCheckSpans = new SpellCheckSpan[size]; 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 if (mLength == mSpellCheckSpans.length) { 188 final int newSize = mLength * 2; 189 int[] newIds = new int[newSize]; 190 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize]; 191 System.arraycopy(mIds, 0, newIds, 0, mLength); 192 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength); 193 mIds = newIds; 194 mSpellCheckSpans = newSpellCheckSpans; 195 } 196 197 mSpellCheckSpans[mLength] = new SpellCheckSpan(); 198 mLength++; 199 return mLength - 1; 200 } 201 addSpellCheckSpan(Editable editable, int start, int end)202 private void addSpellCheckSpan(Editable editable, int start, int end) { 203 final int index = nextSpellCheckSpanIndex(); 204 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[index]; 205 editable.setSpan(spellCheckSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 206 spellCheckSpan.setSpellCheckInProgress(false); 207 mIds[index] = mSpanSequenceCounter++; 208 } 209 onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan)210 public void onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan) { 211 // Recycle any removed SpellCheckSpan (from this code or during text edition) 212 for (int i = 0; i < mLength; i++) { 213 if (mSpellCheckSpans[i] == spellCheckSpan) { 214 mIds[i] = -1; 215 return; 216 } 217 } 218 } 219 onSelectionChanged()220 public void onSelectionChanged() { 221 spellCheck(); 222 } 223 spellCheck(int start, int end)224 public void spellCheck(int start, int end) { 225 if (DBG) { 226 Log.d(TAG, "Start spell-checking: " + start + ", " + end); 227 } 228 final Locale locale = mTextView.getSpellCheckerLocale(); 229 final boolean isSessionActive = isSessionActive(); 230 if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) { 231 setLocale(locale); 232 // Re-check the entire text 233 start = 0; 234 end = mTextView.getText().length(); 235 } else { 236 final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled(); 237 if (isSessionActive != spellCheckerActivated) { 238 // Spell checker has been turned of or off since last spellCheck 239 resetSession(); 240 } 241 } 242 243 if (!isSessionActive) return; 244 245 // Find first available SpellParser from pool 246 final int length = mSpellParsers.length; 247 for (int i = 0; i < length; i++) { 248 final SpellParser spellParser = mSpellParsers[i]; 249 if (spellParser.isFinished()) { 250 spellParser.parse(start, end); 251 return; 252 } 253 } 254 255 if (DBG) { 256 Log.d(TAG, "new spell parser."); 257 } 258 // No available parser found in pool, create a new one 259 SpellParser[] newSpellParsers = new SpellParser[length + 1]; 260 System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length); 261 mSpellParsers = newSpellParsers; 262 263 SpellParser spellParser = new SpellParser(); 264 mSpellParsers[length] = spellParser; 265 spellParser.parse(start, end); 266 } 267 spellCheck()268 private void spellCheck() { 269 if (mSpellCheckerSession == null) return; 270 271 Editable editable = (Editable) mTextView.getText(); 272 final int selectionStart = Selection.getSelectionStart(editable); 273 final int selectionEnd = Selection.getSelectionEnd(editable); 274 275 TextInfo[] textInfos = new TextInfo[mLength]; 276 int textInfosCount = 0; 277 278 for (int i = 0; i < mLength; i++) { 279 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; 280 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue; 281 282 final int start = editable.getSpanStart(spellCheckSpan); 283 final int end = editable.getSpanEnd(spellCheckSpan); 284 285 // Do not check this word if the user is currently editing it 286 final boolean isEditing; 287 if (mIsSentenceSpellCheckSupported) { 288 // Allow the overlap of the cursor and the first boundary of the spell check span 289 // no to skip the spell check of the following word because the 290 // following word will never be spell-checked even if the user finishes composing 291 isEditing = selectionEnd <= start || selectionStart > end; 292 } else { 293 isEditing = selectionEnd < start || selectionStart > end; 294 } 295 if (start >= 0 && end > start && isEditing) { 296 final String word = (editable instanceof SpannableStringBuilder) ? 297 ((SpannableStringBuilder) editable).substring(start, end) : 298 editable.subSequence(start, end).toString(); 299 spellCheckSpan.setSpellCheckInProgress(true); 300 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]); 301 if (DBG) { 302 Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ")" + word 303 + ", 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 <= end) { 735 // Update range span: start new spell check from last wordStart 736 setRangeSpan(editable, wordStart, end); 737 } else { 738 if (DBG && scheduleOtherSpellCheck) { 739 Log.w(TAG, "Trying to schedule spellcheck for invalid region, from " 740 + wordStart + " to " + end); 741 } 742 removeRangeSpan(editable); 743 } 744 745 spellCheck(); 746 } 747 removeSpansAt(Editable editable, int offset, T[] spans)748 private <T> void removeSpansAt(Editable editable, int offset, T[] spans) { 749 final int length = spans.length; 750 for (int i = 0; i < length; i++) { 751 final T span = spans[i]; 752 final int start = editable.getSpanStart(span); 753 if (start > offset) continue; 754 final int end = editable.getSpanEnd(span); 755 if (end < offset) continue; 756 editable.removeSpan(span); 757 } 758 } 759 } 760 haveWordBoundariesChanged(final Editable editable, final int start, final int end, final int spanStart, final int spanEnd)761 public static boolean haveWordBoundariesChanged(final Editable editable, final int start, 762 final int end, final int spanStart, final int spanEnd) { 763 final boolean haveWordBoundariesChanged; 764 if (spanEnd != start && spanStart != end) { 765 haveWordBoundariesChanged = true; 766 if (DBG) { 767 Log.d(TAG, "(1) Text inside the span has been modified. Remove."); 768 } 769 } else if (spanEnd == start && start < editable.length()) { 770 final int codePoint = Character.codePointAt(editable, start); 771 haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint); 772 if (DBG) { 773 Log.d(TAG, "(2) Characters have been appended to the spanned text. " 774 + (haveWordBoundariesChanged ? "Remove.<" : "Keep. <") + (char)(codePoint) 775 + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", " 776 + start); 777 } 778 } else if (spanStart == end && end > 0) { 779 final int codePoint = Character.codePointBefore(editable, end); 780 haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint); 781 if (DBG) { 782 Log.d(TAG, "(3) Characters have been prepended to the spanned text. " 783 + (haveWordBoundariesChanged ? "Remove.<" : "Keep.<") + (char)(codePoint) 784 + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", " 785 + end); 786 } 787 } else { 788 if (DBG) { 789 Log.d(TAG, "(4) Characters adjacent to the spanned text were deleted. Keep."); 790 } 791 haveWordBoundariesChanged = false; 792 } 793 return haveWordBoundariesChanged; 794 } 795 } 796