2121import android .text .Selection ;
2222import android .text .SpannableStringBuilder ;
2323import android .text .Spanned ;
24+ import android .text .TextUtils ;
2425import android .text .method .WordIterator ;
2526import android .text .style .SpellCheckSpan ;
2627import android .text .style .SuggestionSpan ;
2728import android .util .Log ;
29+ import android .util .LruCache ;
2830import android .view .textservice .SentenceSuggestionsInfo ;
2931import android .view .textservice .SpellCheckerSession ;
3032import android .view .textservice .SpellCheckerSession .SpellCheckerSessionListener ;
@@ -95,6 +97,10 @@ public class SpellChecker implements SpellCheckerSessionListener {
9597
9698 private Runnable mSpellRunnable ;
9799
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+
98104 public SpellChecker (TextView textView ) {
99105 mTextView = textView ;
100106
@@ -126,13 +132,15 @@ private void resetSession() {
126132
127133 // Restore SpellCheckSpans in pool
128134 for (int i = 0 ; i < mLength ; i ++) {
135+ // Resets id and progress to invalidate spell check span
129136 mSpellCheckSpans [i ].setSpellCheckInProgress (false );
130137 mIds [i ] = -1 ;
131138 }
132139 mLength = 0 ;
133140
134141 // Remove existing misspelled SuggestionSpans
135142 mTextView .removeMisspelledSpans ((Editable ) mTextView .getText ());
143+ mSuggestionSpanCache .evictAll ();
136144 }
137145
138146 private void setLocale (Locale locale ) {
@@ -199,6 +207,7 @@ private void addSpellCheckSpan(Editable editable, int start, int end) {
199207 public void removeSpellCheckSpan (SpellCheckSpan spellCheckSpan ) {
200208 for (int i = 0 ; i < mLength ; i ++) {
201209 if (mSpellCheckSpans [i ] == spellCheckSpan ) {
210+ // Resets id and progress to invalidate spell check span
202211 mSpellCheckSpans [i ].setSpellCheckInProgress (false );
203212 mIds [i ] = -1 ;
204213 return ;
@@ -211,6 +220,9 @@ public void onSelectionChanged() {
211220 }
212221
213222 public void spellCheck (int start , int end ) {
223+ if (DBG ) {
224+ Log .d (TAG , "Start spell-checking: " + start + ", " + end );
225+ }
214226 final Locale locale = mTextView .getTextServicesLocale ();
215227 final boolean isSessionActive = isSessionActive ();
216228 if (mCurrentLocale == null || (!(mCurrentLocale .equals (locale )))) {
@@ -238,6 +250,9 @@ public void spellCheck(int start, int end) {
238250 }
239251 }
240252
253+ if (DBG ) {
254+ Log .d (TAG , "new spell parser." );
255+ }
241256 // No available parser found in pool, create a new one
242257 SpellParser [] newSpellParsers = new SpellParser [length + 1 ];
243258 System .arraycopy (mSpellParsers , 0 , newSpellParsers , 0 , length );
@@ -260,13 +275,22 @@ private void spellCheck() {
260275
261276 for (int i = 0 ; i < mLength ; i ++) {
262277 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans [i ];
263- if (spellCheckSpan .isSpellCheckInProgress ()) continue ;
278+ if (mIds [ i ] < 0 || spellCheckSpan .isSpellCheckInProgress ()) continue ;
264279
265280 final int start = editable .getSpanStart (spellCheckSpan );
266281 final int end = editable .getSpanEnd (spellCheckSpan );
267282
268283 // Do not check this word if the user is currently editing it
269- if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end )) {
284+ final boolean isEditing ;
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 = selectionEnd <= start || selectionStart > end ;
290+ } else {
291+ isEditing = selectionEnd < start || selectionStart > end ;
292+ }
293+ if (start >= 0 && end > start && isEditing ) {
270294 final String word = (editable instanceof SpannableStringBuilder ) ?
271295 ((SpannableStringBuilder ) editable ).substring (start , end ) :
272296 editable .subSequence (start , end ).toString ();
@@ -372,6 +396,9 @@ public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
372396 }
373397
374398 private void scheduleNewSpellCheck () {
399+ if (DBG ) {
400+ Log .i (TAG , "schedule new spell check." );
401+ }
375402 if (mSpellRunnable == null ) {
376403 mSpellRunnable = new Runnable () {
377404 @ Override
@@ -423,6 +450,20 @@ private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo s
423450
424451 SuggestionSpan suggestionSpan = new SuggestionSpan (mTextView .getContext (), suggestions ,
425452 SuggestionSpan .FLAG_EASY_CORRECT | SuggestionSpan .FLAG_MISSPELLED );
453+ // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
454+ // to share the logic of word level spell checker and sentence level spell checker
455+ if (mIsSentenceSpellCheckSupported ) {
456+ final long key = TextUtils .packRangeInLong (start , end );
457+ final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache .get (key );
458+ if (tempSuggestionSpan != null ) {
459+ if (DBG ) {
460+ Log .i (TAG , "Cached span on the same position is cleard. "
461+ + editable .subSequence (start , end ));
462+ }
463+ editable .removeSpan (tempSuggestionSpan );
464+ }
465+ mSuggestionSpanCache .put (key , suggestionSpan );
466+ }
426467 editable .setSpan (suggestionSpan , start , end , Spanned .SPAN_EXCLUSIVE_EXCLUSIVE );
427468
428469 mTextView .invalidateRegion (start , end , false /* No cursor involved */ );
@@ -447,10 +488,17 @@ public void stop() {
447488 }
448489
449490 private void setRangeSpan (Editable editable , int start , int end ) {
491+ if (DBG ) {
492+ Log .d (TAG , "set next range span: " + start + ", " + end );
493+ }
450494 editable .setSpan (mRange , start , end , Spanned .SPAN_EXCLUSIVE_EXCLUSIVE );
451495 }
452496
453497 private void removeRangeSpan (Editable editable ) {
498+ if (DBG ) {
499+ Log .d (TAG , "Remove range span." + editable .getSpanStart (editable )
500+ + editable .getSpanEnd (editable ));
501+ }
454502 editable .removeSpan (mRange );
455503 }
456504
@@ -484,6 +532,9 @@ public void parse() {
484532 wordEnd = mWordIterator .getEnd (wordStart );
485533 }
486534 if (wordEnd == BreakIterator .DONE ) {
535+ if (DBG ) {
536+ Log .i (TAG , "No more spell check." );
537+ }
487538 removeRangeSpan (editable );
488539 return ;
489540 }
@@ -499,47 +550,72 @@ public void parse() {
499550 boolean scheduleOtherSpellCheck = false ;
500551
501552 if (mIsSentenceSpellCheckSupported ) {
502- int regionEnd ;
503553 if (wordIteratorWindowEnd < end ) {
554+ if (DBG ) {
555+ Log .i (TAG , "schedule other spell check." );
556+ }
504557 // Several batches needed on that region. Cut after last previous word
505- regionEnd = mWordIterator .preceding (wordIteratorWindowEnd );
506558 scheduleOtherSpellCheck = true ;
507- } else {
508- regionEnd = mWordIterator .preceding (end );
509559 }
510- boolean correct = regionEnd != BreakIterator .DONE ;
560+ int spellCheckEnd = mWordIterator .preceding (wordIteratorWindowEnd );
561+ boolean correct = spellCheckEnd != BreakIterator .DONE ;
511562 if (correct ) {
512- regionEnd = mWordIterator .getEnd (regionEnd );
513- correct = regionEnd != BreakIterator .DONE ;
563+ spellCheckEnd = mWordIterator .getEnd (spellCheckEnd );
564+ correct = spellCheckEnd != BreakIterator .DONE ;
514565 }
515566 if (!correct ) {
516- editable .removeSpan (mRange );
517- return ;
518- }
519- // Stop spell checking when there are no characters in the range.
520- if (wordEnd < start ) {
521- return ;
522- }
523- // TODO: Find the start position of the sentence.
524- final int spellCheckStart = wordStart ;
525- if (regionEnd <= spellCheckStart ) {
567+ if (DBG ) {
568+ Log .i (TAG , "Incorrect range span." );
569+ }
570+ removeRangeSpan (editable );
526571 return ;
527572 }
528- final int selectionStart = Selection .getSelectionStart (editable );
529- final int selectionEnd = Selection .getSelectionEnd (editable );
530- if (DBG ) {
531- Log .d (TAG , "addSpellCheckSpan: "
532- + editable .subSequence (spellCheckStart , regionEnd )
533- + ", regionEnd = " + regionEnd + ", spellCheckStart = "
534- + spellCheckStart + ", sel start = " + selectionStart + ", sel end ="
535- + selectionEnd );
536- }
537- // Do not check this word if the user is currently editing it
538- if (spellCheckStart >= 0 && regionEnd > spellCheckStart
539- && (selectionEnd < spellCheckStart || selectionStart > regionEnd )) {
540- addSpellCheckSpan (editable , spellCheckStart , regionEnd );
541- }
542- wordStart = regionEnd ;
573+ do {
574+ // TODO: Find the start position of the sentence.
575+ int spellCheckStart = wordStart ;
576+ boolean createSpellCheckSpan = true ;
577+ // Cancel or merge overlapped spell check spans
578+ for (int i = 0 ; i < mLength ; ++i ) {
579+ final SpellCheckSpan spellCheckSpan = mSpellCheckSpans [i ];
580+ if (mIds [i ] < 0 || spellCheckSpan .isSpellCheckInProgress ()) {
581+ continue ;
582+ }
583+ final int spanStart = editable .getSpanStart (spellCheckSpan );
584+ final int spanEnd = editable .getSpanEnd (spellCheckSpan );
585+ if (spanEnd < spellCheckStart || spellCheckEnd < spanStart ) {
586+ // No need to merge
587+ continue ;
588+ }
589+ if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd ) {
590+ // There is a completely overlapped spell check span
591+ // skip this span
592+ createSpellCheckSpan = false ;
593+ if (DBG ) {
594+ Log .i (TAG , "The range is overrapped. Skip spell check." );
595+ }
596+ break ;
597+ }
598+ removeSpellCheckSpan (spellCheckSpan );
599+ spellCheckStart = Math .min (spanStart , spellCheckStart );
600+ spellCheckEnd = Math .max (spanEnd , spellCheckEnd );
601+ }
602+
603+ if (DBG ) {
604+ Log .d (TAG , "addSpellCheckSpan: "
605+ + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart
606+ + ", next = " + scheduleOtherSpellCheck + "\n "
607+ + editable .subSequence (spellCheckStart , spellCheckEnd ));
608+ }
609+
610+ // Stop spell checking when there are no characters in the range.
611+ if (spellCheckEnd < start ) {
612+ break ;
613+ }
614+ if (createSpellCheckSpan ) {
615+ addSpellCheckSpan (editable , spellCheckStart , spellCheckEnd );
616+ }
617+ } while (false );
618+ wordStart = spellCheckEnd ;
543619 } else {
544620 while (wordStart <= end ) {
545621 if (wordEnd >= start && wordEnd > wordStart ) {
0 commit comments