SpellChecker.java revision a4c82c1c78cdd37e0dea1b5b2f44c25cc584034c
10eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne/*
20eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * Copyright (C) 2011 The Android Open Source Project
30eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne *
40eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * Licensed under the Apache License, Version 2.0 (the "License");
50eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * you may not use this file except in compliance with the License.
60eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * You may obtain a copy of the License at
70eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne *
80eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne *      http://www.apache.org/licenses/LICENSE-2.0
90eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne *
100eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * Unless required by applicable law or agreed to in writing, software
110eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * distributed under the License is distributed on an "AS IS" BASIS,
120eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
130eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * See the License for the specific language governing permissions and
140eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne * limitations under the License.
150eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne */
166435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
176435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunnepackage android.widget;
186435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
196435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.content.Context;
206435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.text.Editable;
216435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.text.Selection;
22653d3a27878d5358b4a91518a756f6b9b3407b07Gilles Debunneimport android.text.SpannableStringBuilder;
236435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.text.Spanned;
248589474d269818713c86ee5e69a685584d1c62e7satokimport android.text.TextUtils;
25287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunneimport android.text.method.WordIterator;
266435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.text.style.SpellCheckSpan;
276435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.text.style.SuggestionSpan;
28e1e874854ab8b73dc5f2346108cbfe90dabaea18satokimport android.util.Log;
298589474d269818713c86ee5e69a685584d1c62e7satokimport android.util.LruCache;
30d404fe110558bd2e1960b428db6a2ee8bfd040cdsatokimport android.view.textservice.SentenceSuggestionsInfo;
316435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.SpellCheckerSession;
326435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
336435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.SuggestionsInfo;
346435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.TextInfo;
356435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.TextServicesManager;
366435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
376435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport com.android.internal.util.ArrayUtils;
386435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
39287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunneimport java.text.BreakIterator;
409d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunneimport java.util.Locale;
41287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
426435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
436435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne/**
446435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne * Helper class for TextView. Bridge between the TextView and the Dictionnary service.
456435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne *
466435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne * @hide
476435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne */
486435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunnepublic class SpellChecker implements SpellCheckerSessionListener {
49e1e874854ab8b73dc5f2346108cbfe90dabaea18satok    private static final String TAG = SpellChecker.class.getSimpleName();
50e1e874854ab8b73dc5f2346108cbfe90dabaea18satok    private static final boolean DBG = false;
516435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
5235199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne    // No more than this number of words will be parsed on each iteration to ensure a minimum
5335199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne    // lock of the UI thread
54be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    public static final int MAX_NUMBER_OF_WORDS = 50;
5535199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne
56be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    // Rough estimate, such that the word iterator interval usually does not need to be shifted
57be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    public static final int AVERAGE_WORD_LENGTH = 7;
5835199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne
5935199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne    // When parsing, use a character window of that size. Will be shifted if needed
6035199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne    public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS;
6135199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne
62be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    // Pause between each spell check to keep the UI smooth
6335199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne    private final static int SPELL_PAUSE_DURATION = 400; // milliseconds
64287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
65979de903695275bae08f2e7a4e99d342f0facd23satok    private static final int MIN_SENTENCE_LENGTH = 50;
66979de903695275bae08f2e7a4e99d342f0facd23satok
678898358bfdf4693af02ad454e1deb8034379ce02satok    private static final int USE_SPAN_RANGE = -1;
688898358bfdf4693af02ad454e1deb8034379ce02satok
696435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    private final TextView mTextView;
706435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
719d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne    SpellCheckerSession mSpellCheckerSession;
728898358bfdf4693af02ad454e1deb8034379ce02satok    // We assume that the sentence level spell check will always provide better results than words.
738898358bfdf4693af02ad454e1deb8034379ce02satok    // Although word SC has a sequential option.
748898358bfdf4693af02ad454e1deb8034379ce02satok    private boolean mIsSentenceSpellCheckSupported;
756435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    final int mCookie;
766435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
77b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne    // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated
78b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne    // SpellCheckSpan has been recycled and can be-reused.
79287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    // Contains null SpellCheckSpans after index mLength.
806435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    private int[] mIds;
816435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    private SpellCheckSpan[] mSpellCheckSpans;
82b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne    // The mLength first elements of the above arrays have been initialized
836435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    private int mLength;
846435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
85287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    // Parsers on chunck of text, cutting text into words that will be checked
86287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    private SpellParser[] mSpellParsers = new SpellParser[0];
87287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
886435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    private int mSpanSequenceCounter = 0;
896435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
909d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne    private Locale mCurrentLocale;
919d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
929d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne    // Shared by all SpellParsers. Cannot be shared with TextView since it may be used
939d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne    // concurrently due to the asynchronous nature of onGetSuggestions.
949d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne    private WordIterator mWordIterator;
959d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
96a49ba2f391cd0753eb12d2707bf9fe128e6566f0Gilles Debunne    private TextServicesManager mTextServicesManager;
97a49ba2f391cd0753eb12d2707bf9fe128e6566f0Gilles Debunne
98be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    private Runnable mSpellRunnable;
99be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne
1008589474d269818713c86ee5e69a685584d1c62e7satok    private static final int SUGGESTION_SPAN_CACHE_SIZE = 10;
1018589474d269818713c86ee5e69a685584d1c62e7satok    private final LruCache<Long, SuggestionSpan> mSuggestionSpanCache =
1028589474d269818713c86ee5e69a685584d1c62e7satok            new LruCache<Long, SuggestionSpan>(SUGGESTION_SPAN_CACHE_SIZE);
1038589474d269818713c86ee5e69a685584d1c62e7satok
1046435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    public SpellChecker(TextView textView) {
1056435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        mTextView = textView;
1066435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
1079d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        // Arbitrary: these arrays will automatically double their sizes on demand
108b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        final int size = ArrayUtils.idealObjectArraySize(1);
1096435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        mIds = new int[size];
1106435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        mSpellCheckSpans = new SpellCheckSpan[size];
1119d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
11205f24700613fb4dce95fb6d5f8fe460d7a30c128satok        setLocale(mTextView.getTextServicesLocale());
1139d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
1149d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        mCookie = hashCode();
1159d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne    }
1169d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
117249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne    private void resetSession() {
118567354474f7893313d63b2bd13d07bf92aa729d3Marco Nelissen        closeSession();
119249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne
120249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        mTextServicesManager = (TextServicesManager) mTextView.getContext().
121249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
122f43305fb057e0818db456065fba9698e2163a762satok        if (!mTextServicesManager.isSpellCheckerEnabled()
123f43305fb057e0818db456065fba9698e2163a762satok                ||  mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) {
1249b3855b75246596fc704825dd92f9269f52cbe64satok            mSpellCheckerSession = null;
1259b3855b75246596fc704825dd92f9269f52cbe64satok        } else {
126249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
1279b3855b75246596fc704825dd92f9269f52cbe64satok                    null /* Bundle not currently used by the textServicesManager */,
128249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                    mCurrentLocale, this,
1299b3855b75246596fc704825dd92f9269f52cbe64satok                    false /* means any available languages from current spell checker */);
130c7ee1b9369ffd7c21a70738056a82dc4238e7fc1satok            mIsSentenceSpellCheckSupported = true;
1319b3855b75246596fc704825dd92f9269f52cbe64satok        }
1329d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
1339d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        // Restore SpellCheckSpans in pool
1349d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        for (int i = 0; i < mLength; i++) {
1358589474d269818713c86ee5e69a685584d1c62e7satok            // Resets id and progress to invalidate spell check span
1369d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            mSpellCheckSpans[i].setSpellCheckInProgress(false);
1379d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            mIds[i] = -1;
1389d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        }
1396435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        mLength = 0;
1409d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
141249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        // Remove existing misspelled SuggestionSpans
142249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        mTextView.removeMisspelledSpans((Editable) mTextView.getText());
1438589474d269818713c86ee5e69a685584d1c62e7satok        mSuggestionSpanCache.evictAll();
144249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne    }
145249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne
146249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne    private void setLocale(Locale locale) {
147249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        mCurrentLocale = locale;
148249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne
149249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        resetSession();
150249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne
151249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        // Change SpellParsers' wordIterator locale
152249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        mWordIterator = new WordIterator(locale);
1539d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
154249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        // This class is the listener for locale change: warn other locale-aware objects
1559d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        mTextView.onLocaleChanged();
1566435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
1576435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
158186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne    /**
159186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne     * @return true if a spell checker session has successfully been created. Returns false if not,
160186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne     * for instance when spell checking has been disabled in settings.
161186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne     */
162287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    private boolean isSessionActive() {
163186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne        return mSpellCheckerSession != null;
164186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne    }
165186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne
166186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne    public void closeSession() {
167186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne        if (mSpellCheckerSession != null) {
168186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne            mSpellCheckerSession.close();
169186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne        }
170287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
171287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        final int length = mSpellParsers.length;
172287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        for (int i = 0; i < length; i++) {
173e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne            mSpellParsers[i].stop();
174287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
175be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne
176be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        if (mSpellRunnable != null) {
177be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            mTextView.removeCallbacks(mSpellRunnable);
178be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        }
179186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne    }
180186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne
181b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne    private int nextSpellCheckSpanIndex() {
182b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        for (int i = 0; i < mLength; i++) {
183b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne            if (mIds[i] < 0) return i;
184b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        }
185b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne
186b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        if (mLength == mSpellCheckSpans.length) {
187b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne            final int newSize = mLength * 2;
1886435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            int[] newIds = new int[newSize];
1896435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize];
190b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne            System.arraycopy(mIds, 0, newIds, 0, mLength);
191b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne            System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength);
1926435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            mIds = newIds;
1936435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            mSpellCheckSpans = newSpellCheckSpans;
1946435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
1956435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
196b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        mSpellCheckSpans[mLength] = new SpellCheckSpan();
1976435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        mLength++;
198b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        return mLength - 1;
199b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne    }
2006435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
201f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne    private void addSpellCheckSpan(Editable editable, int start, int end) {
202b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        final int index = nextSpellCheckSpanIndex();
203f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        editable.setSpan(mSpellCheckSpans[index], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
204b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        mIds[index] = mSpanSequenceCounter++;
2056435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
2066435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2076435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) {
2086435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        for (int i = 0; i < mLength; i++) {
2096435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            if (mSpellCheckSpans[i] == spellCheckSpan) {
2108589474d269818713c86ee5e69a685584d1c62e7satok                // Resets id and progress to invalidate spell check span
211b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne                mSpellCheckSpans[i].setSpellCheckInProgress(false);
212b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne                mIds[i] = -1;
2136435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                return;
2146435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            }
2156435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
2166435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
2176435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2186435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    public void onSelectionChanged() {
219b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        spellCheck();
2206435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
2216435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
222287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    public void spellCheck(int start, int end) {
2238589474d269818713c86ee5e69a685584d1c62e7satok        if (DBG) {
2248589474d269818713c86ee5e69a685584d1c62e7satok            Log.d(TAG, "Start spell-checking: " + start + ", " + end);
2258589474d269818713c86ee5e69a685584d1c62e7satok        }
22605f24700613fb4dce95fb6d5f8fe460d7a30c128satok        final Locale locale = mTextView.getTextServicesLocale();
227c62589cbecef6e748bcc6c6f4ea6a8ff7656923fGilles Debunne        final boolean isSessionActive = isSessionActive();
2289d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        if (mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
2299d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            setLocale(locale);
2309d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            // Re-check the entire text
2319d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            start = 0;
2329d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            end = mTextView.getText().length();
233249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        } else {
234249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
235c62589cbecef6e748bcc6c6f4ea6a8ff7656923fGilles Debunne            if (isSessionActive != spellCheckerActivated) {
236249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                // Spell checker has been turned of or off since last spellCheck
237249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                resetSession();
238249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            }
2399d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        }
2409d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
241c62589cbecef6e748bcc6c6f4ea6a8ff7656923fGilles Debunne        if (!isSessionActive) return;
242287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
243e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        // Find first available SpellParser from pool
244287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        final int length = mSpellParsers.length;
245287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        for (int i = 0; i < length; i++) {
246287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            final SpellParser spellParser = mSpellParsers[i];
247249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            if (spellParser.isFinished()) {
2480249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne                spellParser.parse(start, end);
249287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                return;
250287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
251287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
252287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
2538589474d269818713c86ee5e69a685584d1c62e7satok        if (DBG) {
2548589474d269818713c86ee5e69a685584d1c62e7satok            Log.d(TAG, "new spell parser.");
2558589474d269818713c86ee5e69a685584d1c62e7satok        }
256287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        // No available parser found in pool, create a new one
257287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        SpellParser[] newSpellParsers = new SpellParser[length + 1];
258287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
259287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        mSpellParsers = newSpellParsers;
260287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
261287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        SpellParser spellParser = new SpellParser();
262287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        mSpellParsers[length] = spellParser;
2630249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne        spellParser.parse(start, end);
264287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    }
265287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
266287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    private void spellCheck() {
2670eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne        if (mSpellCheckerSession == null) return;
2689906847cef4307896a64c68fa27da6603a7d8da2Gilles Debunne
269f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        Editable editable = (Editable) mTextView.getText();
270f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        final int selectionStart = Selection.getSelectionStart(editable);
271f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        final int selectionEnd = Selection.getSelectionEnd(editable);
2726435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2736435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        TextInfo[] textInfos = new TextInfo[mLength];
2746435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        int textInfosCount = 0;
2756435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2766435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        for (int i = 0; i < mLength; i++) {
277b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne            final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
2788589474d269818713c86ee5e69a685584d1c62e7satok            if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue;
2796435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
280f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            final int start = editable.getSpanStart(spellCheckSpan);
281f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            final int end = editable.getSpanEnd(spellCheckSpan);
2826435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2836435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            // Do not check this word if the user is currently editing it
2848589474d269818713c86ee5e69a685584d1c62e7satok            final boolean isEditing;
2858589474d269818713c86ee5e69a685584d1c62e7satok            if (mIsSentenceSpellCheckSupported) {
2868589474d269818713c86ee5e69a685584d1c62e7satok                // Allow the overlap of the cursor and the first boundary of the spell check span
2878589474d269818713c86ee5e69a685584d1c62e7satok                // no to skip the spell check of the following word because the
2888589474d269818713c86ee5e69a685584d1c62e7satok                // following word will never be spell-checked even if the user finishes composing
2898589474d269818713c86ee5e69a685584d1c62e7satok                isEditing = selectionEnd <= start || selectionStart > end;
2908589474d269818713c86ee5e69a685584d1c62e7satok            } else {
2918589474d269818713c86ee5e69a685584d1c62e7satok                isEditing = selectionEnd < start || selectionStart > end;
2928589474d269818713c86ee5e69a685584d1c62e7satok            }
2938589474d269818713c86ee5e69a685584d1c62e7satok            if (start >= 0 && end > start && isEditing) {
294653d3a27878d5358b4a91518a756f6b9b3407b07Gilles Debunne                final String word = (editable instanceof SpannableStringBuilder) ?
295653d3a27878d5358b4a91518a756f6b9b3407b07Gilles Debunne                        ((SpannableStringBuilder) editable).substring(start, end) :
296653d3a27878d5358b4a91518a756f6b9b3407b07Gilles Debunne                        editable.subSequence(start, end).toString();
297b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne                spellCheckSpan.setSpellCheckInProgress(true);
2986435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
299e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                if (DBG) {
300e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                    Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ")" + word
301e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                            + ", cookie = " + mCookie + ", seq = "
302e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                            + mIds[i] + ", sel start = " + selectionStart + ", sel end = "
303e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                            + selectionEnd + ", start = " + start + ", end = " + end);
304e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                }
3056435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            }
3066435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
3076435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
3086435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        if (textInfosCount > 0) {
309287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            if (textInfosCount < textInfos.length) {
3106435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
3116435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
3126435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                textInfos = textInfosCopy;
3136435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            }
31435199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne
3158898358bfdf4693af02ad454e1deb8034379ce02satok            if (mIsSentenceSpellCheckSupported) {
3168898358bfdf4693af02ad454e1deb8034379ce02satok                mSpellCheckerSession.getSentenceSuggestions(
3178898358bfdf4693af02ad454e1deb8034379ce02satok                        textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE);
3188898358bfdf4693af02ad454e1deb8034379ce02satok            } else {
3198898358bfdf4693af02ad454e1deb8034379ce02satok                mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
3208898358bfdf4693af02ad454e1deb8034379ce02satok                        false /* TODO Set sequentialWords to true for initial spell check */);
3218898358bfdf4693af02ad454e1deb8034379ce02satok            }
3226435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
3236435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
3246435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
3258898358bfdf4693af02ad454e1deb8034379ce02satok    private SpellCheckSpan onGetSuggestionsInternal(
3268898358bfdf4693af02ad454e1deb8034379ce02satok            SuggestionsInfo suggestionsInfo, int offset, int length) {
327792ee0cc4d9415e45a16803c6fe3e60c53760e25satok        if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) {
3288898358bfdf4693af02ad454e1deb8034379ce02satok            return null;
3298898358bfdf4693af02ad454e1deb8034379ce02satok        }
3308898358bfdf4693af02ad454e1deb8034379ce02satok        final Editable editable = (Editable) mTextView.getText();
3318898358bfdf4693af02ad454e1deb8034379ce02satok        final int sequenceNumber = suggestionsInfo.getSequence();
3328898358bfdf4693af02ad454e1deb8034379ce02satok        for (int k = 0; k < mLength; ++k) {
3338898358bfdf4693af02ad454e1deb8034379ce02satok            if (sequenceNumber == mIds[k]) {
3348898358bfdf4693af02ad454e1deb8034379ce02satok                final int attributes = suggestionsInfo.getSuggestionsAttributes();
3358898358bfdf4693af02ad454e1deb8034379ce02satok                final boolean isInDictionary =
3368898358bfdf4693af02ad454e1deb8034379ce02satok                        ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
3378898358bfdf4693af02ad454e1deb8034379ce02satok                final boolean looksLikeTypo =
3388898358bfdf4693af02ad454e1deb8034379ce02satok                        ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
3398898358bfdf4693af02ad454e1deb8034379ce02satok
3408898358bfdf4693af02ad454e1deb8034379ce02satok                final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k];
3418898358bfdf4693af02ad454e1deb8034379ce02satok                //TODO: we need to change that rule for results from a sentence-level spell
3428898358bfdf4693af02ad454e1deb8034379ce02satok                // checker that will probably be in dictionary.
3438898358bfdf4693af02ad454e1deb8034379ce02satok                if (!isInDictionary && looksLikeTypo) {
3448898358bfdf4693af02ad454e1deb8034379ce02satok                    createMisspelledSuggestionSpan(
3458898358bfdf4693af02ad454e1deb8034379ce02satok                            editable, suggestionsInfo, spellCheckSpan, offset, length);
3468898358bfdf4693af02ad454e1deb8034379ce02satok                }
3478898358bfdf4693af02ad454e1deb8034379ce02satok                return spellCheckSpan;
3488898358bfdf4693af02ad454e1deb8034379ce02satok            }
3498898358bfdf4693af02ad454e1deb8034379ce02satok        }
3508898358bfdf4693af02ad454e1deb8034379ce02satok        return null;
3510dc1f648a09b46c45190ba1ce7daecf7fada4347satok    }
3520dc1f648a09b46c45190ba1ce7daecf7fada4347satok
3530dc1f648a09b46c45190ba1ce7daecf7fada4347satok    @Override
3546435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    public void onGetSuggestions(SuggestionsInfo[] results) {
3558898358bfdf4693af02ad454e1deb8034379ce02satok        final Editable editable = (Editable) mTextView.getText();
3568898358bfdf4693af02ad454e1deb8034379ce02satok        for (int i = 0; i < results.length; ++i) {
3578898358bfdf4693af02ad454e1deb8034379ce02satok            final SpellCheckSpan spellCheckSpan =
3588898358bfdf4693af02ad454e1deb8034379ce02satok                    onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE);
3598898358bfdf4693af02ad454e1deb8034379ce02satok            if (spellCheckSpan != null) {
3608898358bfdf4693af02ad454e1deb8034379ce02satok                editable.removeSpan(spellCheckSpan);
3618898358bfdf4693af02ad454e1deb8034379ce02satok            }
3628898358bfdf4693af02ad454e1deb8034379ce02satok        }
3638898358bfdf4693af02ad454e1deb8034379ce02satok        scheduleNewSpellCheck();
3648898358bfdf4693af02ad454e1deb8034379ce02satok    }
3658615ac9e049cdf4ab77b0897aca9bceec142c9faGilles Debunne
3668898358bfdf4693af02ad454e1deb8034379ce02satok    @Override
3678898358bfdf4693af02ad454e1deb8034379ce02satok    public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
3688898358bfdf4693af02ad454e1deb8034379ce02satok        final Editable editable = (Editable) mTextView.getText();
3698898358bfdf4693af02ad454e1deb8034379ce02satok
3708898358bfdf4693af02ad454e1deb8034379ce02satok        for (int i = 0; i < results.length; ++i) {
3718898358bfdf4693af02ad454e1deb8034379ce02satok            final SentenceSuggestionsInfo ssi = results[i];
372792ee0cc4d9415e45a16803c6fe3e60c53760e25satok            if (ssi == null) {
373792ee0cc4d9415e45a16803c6fe3e60c53760e25satok                continue;
374792ee0cc4d9415e45a16803c6fe3e60c53760e25satok            }
3758898358bfdf4693af02ad454e1deb8034379ce02satok            SpellCheckSpan spellCheckSpan = null;
3768898358bfdf4693af02ad454e1deb8034379ce02satok            for (int j = 0; j < ssi.getSuggestionsCount(); ++j) {
3778898358bfdf4693af02ad454e1deb8034379ce02satok                final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j);
378792ee0cc4d9415e45a16803c6fe3e60c53760e25satok                if (suggestionsInfo == null) {
379792ee0cc4d9415e45a16803c6fe3e60c53760e25satok                    continue;
380792ee0cc4d9415e45a16803c6fe3e60c53760e25satok                }
3818898358bfdf4693af02ad454e1deb8034379ce02satok                final int offset = ssi.getOffsetAt(j);
3828898358bfdf4693af02ad454e1deb8034379ce02satok                final int length = ssi.getLengthAt(j);
3838898358bfdf4693af02ad454e1deb8034379ce02satok                final SpellCheckSpan scs = onGetSuggestionsInternal(
3848898358bfdf4693af02ad454e1deb8034379ce02satok                        suggestionsInfo, offset, length);
3858898358bfdf4693af02ad454e1deb8034379ce02satok                if (spellCheckSpan == null && scs != null) {
3868898358bfdf4693af02ad454e1deb8034379ce02satok                    // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same
3878898358bfdf4693af02ad454e1deb8034379ce02satok                    // SentenceSuggestionsInfo
3888898358bfdf4693af02ad454e1deb8034379ce02satok                    spellCheckSpan = scs;
3896435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                }
3906435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            }
3918898358bfdf4693af02ad454e1deb8034379ce02satok            if (spellCheckSpan != null) {
3928898358bfdf4693af02ad454e1deb8034379ce02satok                editable.removeSpan(spellCheckSpan);
3938898358bfdf4693af02ad454e1deb8034379ce02satok            }
3946435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
395be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        scheduleNewSpellCheck();
396be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    }
397be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne
398be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    private void scheduleNewSpellCheck() {
3998589474d269818713c86ee5e69a685584d1c62e7satok        if (DBG) {
4008589474d269818713c86ee5e69a685584d1c62e7satok            Log.i(TAG, "schedule new spell check.");
4018589474d269818713c86ee5e69a685584d1c62e7satok        }
402be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        if (mSpellRunnable == null) {
403be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            mSpellRunnable = new Runnable() {
404be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                @Override
405be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                public void run() {
406be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                    final int length = mSpellParsers.length;
407be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                    for (int i = 0; i < length; i++) {
408be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                        final SpellParser spellParser = mSpellParsers[i];
409249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                        if (!spellParser.isFinished()) {
410be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                            spellParser.parse();
411be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                            break; // run one spell parser at a time to bound running time
412be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                        }
41335199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne                    }
41435199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne                }
415be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            };
416be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        } else {
417be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            mTextView.removeCallbacks(mSpellRunnable);
418287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
419be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne
420be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
4216435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
4226435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
4238615ac9e049cdf4ab77b0897aca9bceec142c9faGilles Debunne    private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
4248898358bfdf4693af02ad454e1deb8034379ce02satok            SpellCheckSpan spellCheckSpan, int offset, int length) {
4258898358bfdf4693af02ad454e1deb8034379ce02satok        final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
4268898358bfdf4693af02ad454e1deb8034379ce02satok        final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
4278898358bfdf4693af02ad454e1deb8034379ce02satok        if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart)
4288898358bfdf4693af02ad454e1deb8034379ce02satok            return; // span was removed in the meantime
429176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne
430176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne        final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
431176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne        if (suggestionsCount <= 0) {
432176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne            // A negative suggestion count is possible
4336e0b22bea91398403ea500360b02ff2fc7a03d00Gilles Debunne            return;
4346e0b22bea91398403ea500360b02ff2fc7a03d00Gilles Debunne        }
435176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne
4368898358bfdf4693af02ad454e1deb8034379ce02satok        final int start;
4378898358bfdf4693af02ad454e1deb8034379ce02satok        final int end;
4388898358bfdf4693af02ad454e1deb8034379ce02satok        if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
4398898358bfdf4693af02ad454e1deb8034379ce02satok            start = spellCheckSpanStart + offset;
4408898358bfdf4693af02ad454e1deb8034379ce02satok            end = start + length;
4418898358bfdf4693af02ad454e1deb8034379ce02satok        } else {
4428898358bfdf4693af02ad454e1deb8034379ce02satok            start = spellCheckSpanStart;
4438898358bfdf4693af02ad454e1deb8034379ce02satok            end = spellCheckSpanEnd;
4448898358bfdf4693af02ad454e1deb8034379ce02satok        }
4458898358bfdf4693af02ad454e1deb8034379ce02satok
4466e0b22bea91398403ea500360b02ff2fc7a03d00Gilles Debunne        String[] suggestions = new String[suggestionsCount];
4476e0b22bea91398403ea500360b02ff2fc7a03d00Gilles Debunne        for (int i = 0; i < suggestionsCount; i++) {
4486e0b22bea91398403ea500360b02ff2fc7a03d00Gilles Debunne            suggestions[i] = suggestionsInfo.getSuggestionAt(i);
4496435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
450176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne
451176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne        SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
452176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne                SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
4538589474d269818713c86ee5e69a685584d1c62e7satok        // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
4548589474d269818713c86ee5e69a685584d1c62e7satok        // to share the logic of word level spell checker and sentence level spell checker
4558589474d269818713c86ee5e69a685584d1c62e7satok        if (mIsSentenceSpellCheckSupported) {
4568589474d269818713c86ee5e69a685584d1c62e7satok            final long key = TextUtils.packRangeInLong(start, end);
4578589474d269818713c86ee5e69a685584d1c62e7satok            final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
4588589474d269818713c86ee5e69a685584d1c62e7satok            if (tempSuggestionSpan != null) {
4598589474d269818713c86ee5e69a685584d1c62e7satok                if (DBG) {
4608589474d269818713c86ee5e69a685584d1c62e7satok                    Log.i(TAG, "Cached span on the same position is cleard. "
4618589474d269818713c86ee5e69a685584d1c62e7satok                            + editable.subSequence(start, end));
4628589474d269818713c86ee5e69a685584d1c62e7satok                }
4638589474d269818713c86ee5e69a685584d1c62e7satok                editable.removeSpan(tempSuggestionSpan);
4648589474d269818713c86ee5e69a685584d1c62e7satok            }
4658589474d269818713c86ee5e69a685584d1c62e7satok            mSuggestionSpanCache.put(key, suggestionSpan);
4668589474d269818713c86ee5e69a685584d1c62e7satok        }
467f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
468176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne
469961ebb9ab0a6d45b06a74aa90894f7fda3d528c6Gilles Debunne        mTextView.invalidateRegion(start, end, false /* No cursor involved */);
4706435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
471287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
472287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    private class SpellParser {
473287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        private Object mRange = new Object();
474287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
4750249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne        public void parse(int start, int end) {
4760249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne            if (end > start) {
4770249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne                setRangeSpan((Editable) mTextView.getText(), start, end);
4780249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne                parse();
4790249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne            }
480e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        }
481e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne
482249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        public boolean isFinished() {
483249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
484e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        }
485e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne
486249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        public void stop() {
487249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            removeRangeSpan((Editable) mTextView.getText());
488287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
489287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
490e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        private void setRangeSpan(Editable editable, int start, int end) {
4918589474d269818713c86ee5e69a685584d1c62e7satok            if (DBG) {
4928589474d269818713c86ee5e69a685584d1c62e7satok                Log.d(TAG, "set next range span: " + start + ", " + end);
4938589474d269818713c86ee5e69a685584d1c62e7satok            }
494e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne            editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
495287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
496287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
497e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        private void removeRangeSpan(Editable editable) {
4988589474d269818713c86ee5e69a685584d1c62e7satok            if (DBG) {
4998589474d269818713c86ee5e69a685584d1c62e7satok                Log.d(TAG, "Remove range span." + editable.getSpanStart(editable)
5008589474d269818713c86ee5e69a685584d1c62e7satok                        + editable.getSpanEnd(editable));
5018589474d269818713c86ee5e69a685584d1c62e7satok            }
502e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne            editable.removeSpan(mRange);
503287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
504287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
505287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        public void parse() {
506f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            Editable editable = (Editable) mTextView.getText();
507287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            // Iterate over the newly added text and schedule new SpellCheckSpans
50824d146b966c87fd9c3b48027cbfb4238cb892ca5satok            final int start;
50924d146b966c87fd9c3b48027cbfb4238cb892ca5satok            if (mIsSentenceSpellCheckSupported) {
51024d146b966c87fd9c3b48027cbfb4238cb892ca5satok                // TODO: Find the start position of the sentence.
51124d146b966c87fd9c3b48027cbfb4238cb892ca5satok                // Set span with the context
51224d146b966c87fd9c3b48027cbfb4238cb892ca5satok                start =  Math.max(
51324d146b966c87fd9c3b48027cbfb4238cb892ca5satok                        0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH);
51424d146b966c87fd9c3b48027cbfb4238cb892ca5satok            } else {
51524d146b966c87fd9c3b48027cbfb4238cb892ca5satok                start = editable.getSpanStart(mRange);
51624d146b966c87fd9c3b48027cbfb4238cb892ca5satok            }
51724d146b966c87fd9c3b48027cbfb4238cb892ca5satok
518f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            final int end = editable.getSpanEnd(mRange);
51935199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne
52035199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne            int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
521be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
522287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
523287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            // Move back to the beginning of the current word, if any
524287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            int wordStart = mWordIterator.preceding(start);
525287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            int wordEnd;
526287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            if (wordStart == BreakIterator.DONE) {
527287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                wordEnd = mWordIterator.following(start);
528287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                if (wordEnd != BreakIterator.DONE) {
529287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                    wordStart = mWordIterator.getBeginning(wordEnd);
530287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                }
531287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            } else {
532287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                wordEnd = mWordIterator.getEnd(wordStart);
533287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
534287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            if (wordEnd == BreakIterator.DONE) {
5358589474d269818713c86ee5e69a685584d1c62e7satok                if (DBG) {
5368589474d269818713c86ee5e69a685584d1c62e7satok                    Log.i(TAG, "No more spell check.");
5378589474d269818713c86ee5e69a685584d1c62e7satok                }
538e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne                removeRangeSpan(editable);
539287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                return;
540287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
541287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
542287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            // We need to expand by one character because we want to include the spans that
543287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            // end/start at position start/end respectively.
544f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
545f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                    SpellCheckSpan.class);
546f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
547f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                    SuggestionSpan.class);
548287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
54935199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne            int wordCount = 0;
550287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            boolean scheduleOtherSpellCheck = false;
551287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
5528898358bfdf4693af02ad454e1deb8034379ce02satok            if (mIsSentenceSpellCheckSupported) {
5538898358bfdf4693af02ad454e1deb8034379ce02satok                if (wordIteratorWindowEnd < end) {
5548589474d269818713c86ee5e69a685584d1c62e7satok                    if (DBG) {
5558589474d269818713c86ee5e69a685584d1c62e7satok                        Log.i(TAG, "schedule other spell check.");
5568589474d269818713c86ee5e69a685584d1c62e7satok                    }
5578898358bfdf4693af02ad454e1deb8034379ce02satok                    // Several batches needed on that region. Cut after last previous word
5588898358bfdf4693af02ad454e1deb8034379ce02satok                    scheduleOtherSpellCheck = true;
5598898358bfdf4693af02ad454e1deb8034379ce02satok                }
5608589474d269818713c86ee5e69a685584d1c62e7satok                int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd);
5618589474d269818713c86ee5e69a685584d1c62e7satok                boolean correct = spellCheckEnd != BreakIterator.DONE;
5628898358bfdf4693af02ad454e1deb8034379ce02satok                if (correct) {
5638589474d269818713c86ee5e69a685584d1c62e7satok                    spellCheckEnd = mWordIterator.getEnd(spellCheckEnd);
5648589474d269818713c86ee5e69a685584d1c62e7satok                    correct = spellCheckEnd != BreakIterator.DONE;
5658898358bfdf4693af02ad454e1deb8034379ce02satok                }
5668898358bfdf4693af02ad454e1deb8034379ce02satok                if (!correct) {
5678589474d269818713c86ee5e69a685584d1c62e7satok                    if (DBG) {
5688589474d269818713c86ee5e69a685584d1c62e7satok                        Log.i(TAG, "Incorrect range span.");
5698589474d269818713c86ee5e69a685584d1c62e7satok                    }
5708589474d269818713c86ee5e69a685584d1c62e7satok                    removeRangeSpan(editable);
5718898358bfdf4693af02ad454e1deb8034379ce02satok                    return;
5728898358bfdf4693af02ad454e1deb8034379ce02satok                }
5738589474d269818713c86ee5e69a685584d1c62e7satok                do {
5748589474d269818713c86ee5e69a685584d1c62e7satok                    // TODO: Find the start position of the sentence.
5758589474d269818713c86ee5e69a685584d1c62e7satok                    int spellCheckStart = wordStart;
5768589474d269818713c86ee5e69a685584d1c62e7satok                    boolean createSpellCheckSpan = true;
5778589474d269818713c86ee5e69a685584d1c62e7satok                    // Cancel or merge overlapped spell check spans
5788589474d269818713c86ee5e69a685584d1c62e7satok                    for (int i = 0; i < mLength; ++i) {
5798589474d269818713c86ee5e69a685584d1c62e7satok                        final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
5808589474d269818713c86ee5e69a685584d1c62e7satok                        if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) {
5818589474d269818713c86ee5e69a685584d1c62e7satok                            continue;
5828589474d269818713c86ee5e69a685584d1c62e7satok                        }
5838589474d269818713c86ee5e69a685584d1c62e7satok                        final int spanStart = editable.getSpanStart(spellCheckSpan);
5848589474d269818713c86ee5e69a685584d1c62e7satok                        final int spanEnd = editable.getSpanEnd(spellCheckSpan);
5858589474d269818713c86ee5e69a685584d1c62e7satok                        if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) {
5868589474d269818713c86ee5e69a685584d1c62e7satok                            // No need to merge
5878589474d269818713c86ee5e69a685584d1c62e7satok                            continue;
5888589474d269818713c86ee5e69a685584d1c62e7satok                        }
5898589474d269818713c86ee5e69a685584d1c62e7satok                        if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) {
5908589474d269818713c86ee5e69a685584d1c62e7satok                            // There is a completely overlapped spell check span
5918589474d269818713c86ee5e69a685584d1c62e7satok                            // skip this span
5928589474d269818713c86ee5e69a685584d1c62e7satok                            createSpellCheckSpan = false;
5938589474d269818713c86ee5e69a685584d1c62e7satok                            if (DBG) {
5948589474d269818713c86ee5e69a685584d1c62e7satok                                Log.i(TAG, "The range is overrapped. Skip spell check.");
5958589474d269818713c86ee5e69a685584d1c62e7satok                            }
5968589474d269818713c86ee5e69a685584d1c62e7satok                            break;
5978589474d269818713c86ee5e69a685584d1c62e7satok                        }
5988589474d269818713c86ee5e69a685584d1c62e7satok                        removeSpellCheckSpan(spellCheckSpan);
5998589474d269818713c86ee5e69a685584d1c62e7satok                        spellCheckStart = Math.min(spanStart, spellCheckStart);
6008589474d269818713c86ee5e69a685584d1c62e7satok                        spellCheckEnd = Math.max(spanEnd, spellCheckEnd);
6018589474d269818713c86ee5e69a685584d1c62e7satok                    }
6028589474d269818713c86ee5e69a685584d1c62e7satok
6038589474d269818713c86ee5e69a685584d1c62e7satok                    if (DBG) {
6048589474d269818713c86ee5e69a685584d1c62e7satok                        Log.d(TAG, "addSpellCheckSpan: "
6058589474d269818713c86ee5e69a685584d1c62e7satok                                + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart
6068589474d269818713c86ee5e69a685584d1c62e7satok                                + ", next = " + scheduleOtherSpellCheck + "\n"
6078589474d269818713c86ee5e69a685584d1c62e7satok                                + editable.subSequence(spellCheckStart, spellCheckEnd));
6088589474d269818713c86ee5e69a685584d1c62e7satok                    }
6098589474d269818713c86ee5e69a685584d1c62e7satok
6108589474d269818713c86ee5e69a685584d1c62e7satok                    // Stop spell checking when there are no characters in the range.
6118589474d269818713c86ee5e69a685584d1c62e7satok                    if (spellCheckEnd < start) {
6128589474d269818713c86ee5e69a685584d1c62e7satok                        break;
6138589474d269818713c86ee5e69a685584d1c62e7satok                    }
614a4c82c1c78cdd37e0dea1b5b2f44c25cc584034csatok                    if (spellCheckEnd <= spellCheckStart) {
615a4c82c1c78cdd37e0dea1b5b2f44c25cc584034csatok                        break;
616a4c82c1c78cdd37e0dea1b5b2f44c25cc584034csatok                    }
6178589474d269818713c86ee5e69a685584d1c62e7satok                    if (createSpellCheckSpan) {
6188589474d269818713c86ee5e69a685584d1c62e7satok                        addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd);
6198589474d269818713c86ee5e69a685584d1c62e7satok                    }
6208589474d269818713c86ee5e69a685584d1c62e7satok                } while (false);
6218589474d269818713c86ee5e69a685584d1c62e7satok                wordStart = spellCheckEnd;
6228898358bfdf4693af02ad454e1deb8034379ce02satok            } else {
6238898358bfdf4693af02ad454e1deb8034379ce02satok                while (wordStart <= end) {
6248898358bfdf4693af02ad454e1deb8034379ce02satok                    if (wordEnd >= start && wordEnd > wordStart) {
6258898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordCount >= MAX_NUMBER_OF_WORDS) {
6268898358bfdf4693af02ad454e1deb8034379ce02satok                            scheduleOtherSpellCheck = true;
6278898358bfdf4693af02ad454e1deb8034379ce02satok                            break;
6288898358bfdf4693af02ad454e1deb8034379ce02satok                        }
6298898358bfdf4693af02ad454e1deb8034379ce02satok                        // A new word has been created across the interval boundaries with this
6308898358bfdf4693af02ad454e1deb8034379ce02satok                        // edit. The previous spans (that ended on start / started on end) are
6318898358bfdf4693af02ad454e1deb8034379ce02satok                        // not valid anymore and must be removed.
6328898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordStart < start && wordEnd > start) {
6338898358bfdf4693af02ad454e1deb8034379ce02satok                            removeSpansAt(editable, start, spellCheckSpans);
6348898358bfdf4693af02ad454e1deb8034379ce02satok                            removeSpansAt(editable, start, suggestionSpans);
6358898358bfdf4693af02ad454e1deb8034379ce02satok                        }
636287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
6378898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordStart < end && wordEnd > end) {
6388898358bfdf4693af02ad454e1deb8034379ce02satok                            removeSpansAt(editable, end, spellCheckSpans);
6398898358bfdf4693af02ad454e1deb8034379ce02satok                            removeSpansAt(editable, end, suggestionSpans);
6408898358bfdf4693af02ad454e1deb8034379ce02satok                        }
641287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
6428898358bfdf4693af02ad454e1deb8034379ce02satok                        // Do not create new boundary spans if they already exist
6438898358bfdf4693af02ad454e1deb8034379ce02satok                        boolean createSpellCheckSpan = true;
6448898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordEnd == start) {
6458898358bfdf4693af02ad454e1deb8034379ce02satok                            for (int i = 0; i < spellCheckSpans.length; i++) {
6468898358bfdf4693af02ad454e1deb8034379ce02satok                                final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
6478898358bfdf4693af02ad454e1deb8034379ce02satok                                if (spanEnd == start) {
6488898358bfdf4693af02ad454e1deb8034379ce02satok                                    createSpellCheckSpan = false;
6498898358bfdf4693af02ad454e1deb8034379ce02satok                                    break;
6508898358bfdf4693af02ad454e1deb8034379ce02satok                                }
651287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                            }
652287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                        }
653287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
6548898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordStart == end) {
6558898358bfdf4693af02ad454e1deb8034379ce02satok                            for (int i = 0; i < spellCheckSpans.length; i++) {
6568898358bfdf4693af02ad454e1deb8034379ce02satok                                final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
6578898358bfdf4693af02ad454e1deb8034379ce02satok                                if (spanStart == end) {
6588898358bfdf4693af02ad454e1deb8034379ce02satok                                    createSpellCheckSpan = false;
6598898358bfdf4693af02ad454e1deb8034379ce02satok                                    break;
6608898358bfdf4693af02ad454e1deb8034379ce02satok                                }
661287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                            }
662287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                        }
663287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
6648898358bfdf4693af02ad454e1deb8034379ce02satok                        if (createSpellCheckSpan) {
6658898358bfdf4693af02ad454e1deb8034379ce02satok                            addSpellCheckSpan(editable, wordStart, wordEnd);
6668898358bfdf4693af02ad454e1deb8034379ce02satok                        }
6678898358bfdf4693af02ad454e1deb8034379ce02satok                        wordCount++;
668287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                    }
669287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
6708898358bfdf4693af02ad454e1deb8034379ce02satok                    // iterate word by word
6718898358bfdf4693af02ad454e1deb8034379ce02satok                    int originalWordEnd = wordEnd;
6728898358bfdf4693af02ad454e1deb8034379ce02satok                    wordEnd = mWordIterator.following(wordEnd);
6738898358bfdf4693af02ad454e1deb8034379ce02satok                    if ((wordIteratorWindowEnd < end) &&
6748898358bfdf4693af02ad454e1deb8034379ce02satok                            (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
6758898358bfdf4693af02ad454e1deb8034379ce02satok                        wordIteratorWindowEnd =
6768898358bfdf4693af02ad454e1deb8034379ce02satok                                Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
6778898358bfdf4693af02ad454e1deb8034379ce02satok                        mWordIterator.setCharSequence(
6788898358bfdf4693af02ad454e1deb8034379ce02satok                                editable, originalWordEnd, wordIteratorWindowEnd);
6798898358bfdf4693af02ad454e1deb8034379ce02satok                        wordEnd = mWordIterator.following(originalWordEnd);
6808898358bfdf4693af02ad454e1deb8034379ce02satok                    }
6818898358bfdf4693af02ad454e1deb8034379ce02satok                    if (wordEnd == BreakIterator.DONE) break;
6828898358bfdf4693af02ad454e1deb8034379ce02satok                    wordStart = mWordIterator.getBeginning(wordEnd);
6838898358bfdf4693af02ad454e1deb8034379ce02satok                    if (wordStart == BreakIterator.DONE) {
6848898358bfdf4693af02ad454e1deb8034379ce02satok                        break;
6858898358bfdf4693af02ad454e1deb8034379ce02satok                    }
686287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                }
687287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
688287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
689287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            if (scheduleOtherSpellCheck) {
690e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne                // Update range span: start new spell check from last wordStart
691e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne                setRangeSpan(editable, wordStart, end);
692287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            } else {
693e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne                removeRangeSpan(editable);
694287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
695287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
696287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            spellCheck();
697287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
698287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
699f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        private <T> void removeSpansAt(Editable editable, int offset, T[] spans) {
700287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            final int length = spans.length;
701287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            for (int i = 0; i < length; i++) {
702287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                final T span = spans[i];
703f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                final int start = editable.getSpanStart(span);
704287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                if (start > offset) continue;
705f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                final int end = editable.getSpanEnd(span);
706287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                if (end < offset) continue;
707f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                editable.removeSpan(span);
708287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
709287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
710287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    }
7116435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne}
712