• 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 && 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