• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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) {
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 }
757