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;
226435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.text.Spanned;
238589474d269818713c86ee5e69a685584d1c62e7satokimport android.text.TextUtils;
24287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunneimport android.text.method.WordIterator;
256435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.text.style.SpellCheckSpan;
266435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.text.style.SuggestionSpan;
27e1e874854ab8b73dc5f2346108cbfe90dabaea18satokimport android.util.Log;
288589474d269818713c86ee5e69a685584d1c62e7satokimport android.util.LruCache;
29d404fe110558bd2e1960b428db6a2ee8bfd040cdsatokimport android.view.textservice.SentenceSuggestionsInfo;
306435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.SpellCheckerSession;
316435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
326435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.SuggestionsInfo;
336435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.TextInfo;
346435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport android.view.textservice.TextServicesManager;
356435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
366435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunneimport com.android.internal.util.ArrayUtils;
37776abc24cdd18610232a50b997cce3cffa74609bAdam Lesinskiimport com.android.internal.util.GrowingArrayUtils;
386435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
39287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunneimport java.text.BreakIterator;
409d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunneimport java.util.Locale;
41287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
426435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
436435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne/**
445d6b6f2892c90e95ef3bc650a245a5f2ca021d38Yohei Yukawa * Helper class for TextView. Bridge between the TextView and the Dictionary 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
855d6b6f2892c90e95ef3bc650a245a5f2ca021d38Yohei Yukawa    // Parsers on chunk 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
108776abc24cdd18610232a50b997cce3cffa74609bAdam Lesinski        final int size = 1;
109776abc24cdd18610232a50b997cce3cffa74609bAdam Lesinski        mIds = ArrayUtils.newUnpaddedIntArray(size);
110776abc24cdd18610232a50b997cce3cffa74609bAdam Lesinski        mSpellCheckSpans = new SpellCheckSpan[mIds.length];
1119d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
1125bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka        setLocale(mTextView.getSpellCheckerLocale());
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()
1235bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka                || mCurrentLocale == null
1245bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka                || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) {
1259b3855b75246596fc704825dd92f9269f52cbe64satok            mSpellCheckerSession = null;
1269b3855b75246596fc704825dd92f9269f52cbe64satok        } else {
127249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
1289b3855b75246596fc704825dd92f9269f52cbe64satok                    null /* Bundle not currently used by the textServicesManager */,
129249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                    mCurrentLocale, this,
1309b3855b75246596fc704825dd92f9269f52cbe64satok                    false /* means any available languages from current spell checker */);
131c7ee1b9369ffd7c21a70738056a82dc4238e7fc1satok            mIsSentenceSpellCheckSupported = true;
1329b3855b75246596fc704825dd92f9269f52cbe64satok        }
1339d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
1349d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        // Restore SpellCheckSpans in pool
1359d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        for (int i = 0; i < mLength; i++) {
1369d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            mIds[i] = -1;
1379d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        }
1386435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        mLength = 0;
1399d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
140249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        // Remove existing misspelled SuggestionSpans
141249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        mTextView.removeMisspelledSpans((Editable) mTextView.getText());
1428589474d269818713c86ee5e69a685584d1c62e7satok        mSuggestionSpanCache.evictAll();
143249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne    }
144249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne
145249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne    private void setLocale(Locale locale) {
146249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        mCurrentLocale = locale;
147249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne
148249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        resetSession();
149249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne
1505bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka        if (locale != null) {
1515bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka            // Change SpellParsers' wordIterator locale
1525bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka            mWordIterator = new WordIterator(locale);
1535bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka        }
1549d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
155249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        // This class is the listener for locale change: warn other locale-aware objects
1569d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        mTextView.onLocaleChanged();
1576435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
1586435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
159186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne    /**
160186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne     * @return true if a spell checker session has successfully been created. Returns false if not,
161186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne     * for instance when spell checking has been disabled in settings.
162186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne     */
163287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    private boolean isSessionActive() {
164186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne        return mSpellCheckerSession != null;
165186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne    }
166186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne
167186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne    public void closeSession() {
168186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne        if (mSpellCheckerSession != null) {
169186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne            mSpellCheckerSession.close();
170186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne        }
171287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
172287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        final int length = mSpellParsers.length;
173287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        for (int i = 0; i < length; i++) {
174e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne            mSpellParsers[i].stop();
175287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
176be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne
177be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        if (mSpellRunnable != null) {
178be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            mTextView.removeCallbacks(mSpellRunnable);
179be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        }
180186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne    }
181186aaf973530f426b9b0e602e9744c591aa4aea9Gilles Debunne
182b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne    private int nextSpellCheckSpanIndex() {
183b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        for (int i = 0; i < mLength; i++) {
184b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne            if (mIds[i] < 0) return i;
185b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        }
186b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne
187776abc24cdd18610232a50b997cce3cffa74609bAdam Lesinski        mIds = GrowingArrayUtils.append(mIds, mLength, 0);
188776abc24cdd18610232a50b997cce3cffa74609bAdam Lesinski        mSpellCheckSpans = GrowingArrayUtils.append(
189776abc24cdd18610232a50b997cce3cffa74609bAdam Lesinski                mSpellCheckSpans, mLength, new SpellCheckSpan());
1906435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        mLength++;
191b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        return mLength - 1;
192b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne    }
1936435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
194f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne    private void addSpellCheckSpan(Editable editable, int start, int end) {
195b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        final int index = nextSpellCheckSpanIndex();
19669865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne        SpellCheckSpan spellCheckSpan = mSpellCheckSpans[index];
19769865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne        editable.setSpan(spellCheckSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
19869865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne        spellCheckSpan.setSpellCheckInProgress(false);
199b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        mIds[index] = mSpanSequenceCounter++;
2006435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
2016435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
20269865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne    public void onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan) {
20369865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne        // Recycle any removed SpellCheckSpan (from this code or during text edition)
2046435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        for (int i = 0; i < mLength; i++) {
2056435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            if (mSpellCheckSpans[i] == spellCheckSpan) {
206b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne                mIds[i] = -1;
2076435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                return;
2086435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            }
2096435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
2106435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
2116435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2126435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    public void onSelectionChanged() {
213b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne        spellCheck();
2146435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
2156435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
216287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    public void spellCheck(int start, int end) {
2178589474d269818713c86ee5e69a685584d1c62e7satok        if (DBG) {
2188589474d269818713c86ee5e69a685584d1c62e7satok            Log.d(TAG, "Start spell-checking: " + start + ", " + end);
2198589474d269818713c86ee5e69a685584d1c62e7satok        }
2205bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka        final Locale locale = mTextView.getSpellCheckerLocale();
221c62589cbecef6e748bcc6c6f4ea6a8ff7656923fGilles Debunne        final boolean isSessionActive = isSessionActive();
2225bb4ee6d38bec37c84086d52a2293b5396ee33dfSatoshi Kataoka        if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
2239d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            setLocale(locale);
2249d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            // Re-check the entire text
2259d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            start = 0;
2269d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne            end = mTextView.getText().length();
227249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        } else {
228249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
229c62589cbecef6e748bcc6c6f4ea6a8ff7656923fGilles Debunne            if (isSessionActive != spellCheckerActivated) {
230249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                // Spell checker has been turned of or off since last spellCheck
231249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                resetSession();
232249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            }
2339d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne        }
2349d8d3f1539ce5bdf512bd47ec1648609d6cde5b1Gilles Debunne
235c62589cbecef6e748bcc6c6f4ea6a8ff7656923fGilles Debunne        if (!isSessionActive) return;
236287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
237e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        // Find first available SpellParser from pool
238287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        final int length = mSpellParsers.length;
239287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        for (int i = 0; i < length; i++) {
240287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            final SpellParser spellParser = mSpellParsers[i];
241249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            if (spellParser.isFinished()) {
2420249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne                spellParser.parse(start, end);
243287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                return;
244287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
245287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
246287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
2478589474d269818713c86ee5e69a685584d1c62e7satok        if (DBG) {
2488589474d269818713c86ee5e69a685584d1c62e7satok            Log.d(TAG, "new spell parser.");
2498589474d269818713c86ee5e69a685584d1c62e7satok        }
250287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        // No available parser found in pool, create a new one
251287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        SpellParser[] newSpellParsers = new SpellParser[length + 1];
252287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
253287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        mSpellParsers = newSpellParsers;
254287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
255287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        SpellParser spellParser = new SpellParser();
256287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        mSpellParsers[length] = spellParser;
2570249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne        spellParser.parse(start, end);
258287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    }
259287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
260287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    private void spellCheck() {
2610eea6681519277310e1733d791bfc0342b8e5ceaGilles Debunne        if (mSpellCheckerSession == null) return;
2629906847cef4307896a64c68fa27da6603a7d8da2Gilles Debunne
263f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        Editable editable = (Editable) mTextView.getText();
264f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        final int selectionStart = Selection.getSelectionStart(editable);
265f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        final int selectionEnd = Selection.getSelectionEnd(editable);
2666435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2676435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        TextInfo[] textInfos = new TextInfo[mLength];
2686435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        int textInfosCount = 0;
2696435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2706435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        for (int i = 0; i < mLength; i++) {
271b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne            final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
2728589474d269818713c86ee5e69a685584d1c62e7satok            if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue;
2736435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
274f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            final int start = editable.getSpanStart(spellCheckSpan);
275f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            final int end = editable.getSpanEnd(spellCheckSpan);
2766435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
2776435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            // Do not check this word if the user is currently editing it
2788589474d269818713c86ee5e69a685584d1c62e7satok            final boolean isEditing;
279b1fef1114e31e35c90bc885fd9e8f9381895846fRaph Levien
280b1fef1114e31e35c90bc885fd9e8f9381895846fRaph Levien            // Defer spell check when typing a word with an interior apostrophe.
281b1fef1114e31e35c90bc885fd9e8f9381895846fRaph Levien            // TODO: a better solution to this would be to make the word
282b1fef1114e31e35c90bc885fd9e8f9381895846fRaph Levien            // iterator locale-sensitive and include the apostrophe in
283b1fef1114e31e35c90bc885fd9e8f9381895846fRaph Levien            // languages that use it (such as English).
284b1fef1114e31e35c90bc885fd9e8f9381895846fRaph Levien            final boolean apostrophe = (selectionStart == end + 1 && editable.charAt(end) == '\'');
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
289b1fef1114e31e35c90bc885fd9e8f9381895846fRaph Levien                isEditing = !apostrophe && (selectionEnd <= start || selectionStart > end);
2908589474d269818713c86ee5e69a685584d1c62e7satok            } else {
291b1fef1114e31e35c90bc885fd9e8f9381895846fRaph Levien                isEditing = !apostrophe && (selectionEnd < start || selectionStart > end);
2928589474d269818713c86ee5e69a685584d1c62e7satok            }
2938589474d269818713c86ee5e69a685584d1c62e7satok            if (start >= 0 && end > start && isEditing) {
294b062e81e3a16af43db3619d721aa522c137d1aa9Gilles Debunne                spellCheckSpan.setSpellCheckInProgress(true);
2955d6b6f2892c90e95ef3bc650a245a5f2ca021d38Yohei Yukawa                final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]);
2965d6b6f2892c90e95ef3bc650a245a5f2ca021d38Yohei Yukawa                textInfos[textInfosCount++] = textInfo;
297e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                if (DBG) {
2985d6b6f2892c90e95ef3bc650a245a5f2ca021d38Yohei Yukawa                    Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ") text = "
2995d6b6f2892c90e95ef3bc650a245a5f2ca021d38Yohei Yukawa                            + textInfo.getSequence() + ", cookie = " + mCookie + ", seq = "
300e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                            + mIds[i] + ", sel start = " + selectionStart + ", sel end = "
301e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                            + selectionEnd + ", start = " + start + ", end = " + end);
302e1e874854ab8b73dc5f2346108cbfe90dabaea18satok                }
3036435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            }
3046435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
3056435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
3066435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        if (textInfosCount > 0) {
307287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            if (textInfosCount < textInfos.length) {
3086435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
3096435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
3106435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                textInfos = textInfosCopy;
3116435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            }
31235199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne
3138898358bfdf4693af02ad454e1deb8034379ce02satok            if (mIsSentenceSpellCheckSupported) {
3148898358bfdf4693af02ad454e1deb8034379ce02satok                mSpellCheckerSession.getSentenceSuggestions(
3158898358bfdf4693af02ad454e1deb8034379ce02satok                        textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE);
3168898358bfdf4693af02ad454e1deb8034379ce02satok            } else {
3178898358bfdf4693af02ad454e1deb8034379ce02satok                mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
3188898358bfdf4693af02ad454e1deb8034379ce02satok                        false /* TODO Set sequentialWords to true for initial spell check */);
3198898358bfdf4693af02ad454e1deb8034379ce02satok            }
3206435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
3216435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
3226435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
3238898358bfdf4693af02ad454e1deb8034379ce02satok    private SpellCheckSpan onGetSuggestionsInternal(
3248898358bfdf4693af02ad454e1deb8034379ce02satok            SuggestionsInfo suggestionsInfo, int offset, int length) {
325792ee0cc4d9415e45a16803c6fe3e60c53760e25satok        if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) {
3268898358bfdf4693af02ad454e1deb8034379ce02satok            return null;
3278898358bfdf4693af02ad454e1deb8034379ce02satok        }
3288898358bfdf4693af02ad454e1deb8034379ce02satok        final Editable editable = (Editable) mTextView.getText();
3298898358bfdf4693af02ad454e1deb8034379ce02satok        final int sequenceNumber = suggestionsInfo.getSequence();
3308898358bfdf4693af02ad454e1deb8034379ce02satok        for (int k = 0; k < mLength; ++k) {
3318898358bfdf4693af02ad454e1deb8034379ce02satok            if (sequenceNumber == mIds[k]) {
3328898358bfdf4693af02ad454e1deb8034379ce02satok                final int attributes = suggestionsInfo.getSuggestionsAttributes();
3338898358bfdf4693af02ad454e1deb8034379ce02satok                final boolean isInDictionary =
3348898358bfdf4693af02ad454e1deb8034379ce02satok                        ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
3358898358bfdf4693af02ad454e1deb8034379ce02satok                final boolean looksLikeTypo =
3368898358bfdf4693af02ad454e1deb8034379ce02satok                        ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
3378898358bfdf4693af02ad454e1deb8034379ce02satok
3388898358bfdf4693af02ad454e1deb8034379ce02satok                final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k];
3398898358bfdf4693af02ad454e1deb8034379ce02satok                //TODO: we need to change that rule for results from a sentence-level spell
3408898358bfdf4693af02ad454e1deb8034379ce02satok                // checker that will probably be in dictionary.
3418898358bfdf4693af02ad454e1deb8034379ce02satok                if (!isInDictionary && looksLikeTypo) {
3428898358bfdf4693af02ad454e1deb8034379ce02satok                    createMisspelledSuggestionSpan(
3438898358bfdf4693af02ad454e1deb8034379ce02satok                            editable, suggestionsInfo, spellCheckSpan, offset, length);
344bec154c50036bc70a37518dc93f6821209f58728satok                } else {
345bec154c50036bc70a37518dc93f6821209f58728satok                    // Valid word -- isInDictionary || !looksLikeTypo
346bec154c50036bc70a37518dc93f6821209f58728satok                    if (mIsSentenceSpellCheckSupported) {
347bec154c50036bc70a37518dc93f6821209f58728satok                        // Allow the spell checker to remove existing misspelled span by
348bec154c50036bc70a37518dc93f6821209f58728satok                        // overwriting the span over the same place
349bec154c50036bc70a37518dc93f6821209f58728satok                        final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
350bec154c50036bc70a37518dc93f6821209f58728satok                        final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
351bec154c50036bc70a37518dc93f6821209f58728satok                        final int start;
352bec154c50036bc70a37518dc93f6821209f58728satok                        final int end;
353bec154c50036bc70a37518dc93f6821209f58728satok                        if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
354bec154c50036bc70a37518dc93f6821209f58728satok                            start = spellCheckSpanStart + offset;
355bec154c50036bc70a37518dc93f6821209f58728satok                            end = start + length;
356bec154c50036bc70a37518dc93f6821209f58728satok                        } else {
357bec154c50036bc70a37518dc93f6821209f58728satok                            start = spellCheckSpanStart;
358bec154c50036bc70a37518dc93f6821209f58728satok                            end = spellCheckSpanEnd;
359bec154c50036bc70a37518dc93f6821209f58728satok                        }
360bec154c50036bc70a37518dc93f6821209f58728satok                        if (spellCheckSpanStart >= 0 && spellCheckSpanEnd > spellCheckSpanStart
361bec154c50036bc70a37518dc93f6821209f58728satok                                && end > start) {
362bec154c50036bc70a37518dc93f6821209f58728satok                            final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
363bec154c50036bc70a37518dc93f6821209f58728satok                            final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
364bec154c50036bc70a37518dc93f6821209f58728satok                            if (tempSuggestionSpan != null) {
365bec154c50036bc70a37518dc93f6821209f58728satok                                if (DBG) {
366bec154c50036bc70a37518dc93f6821209f58728satok                                    Log.i(TAG, "Remove existing misspelled span. "
367bec154c50036bc70a37518dc93f6821209f58728satok                                            + editable.subSequence(start, end));
368bec154c50036bc70a37518dc93f6821209f58728satok                                }
369bec154c50036bc70a37518dc93f6821209f58728satok                                editable.removeSpan(tempSuggestionSpan);
370bec154c50036bc70a37518dc93f6821209f58728satok                                mSuggestionSpanCache.remove(key);
371bec154c50036bc70a37518dc93f6821209f58728satok                            }
372bec154c50036bc70a37518dc93f6821209f58728satok                        }
373bec154c50036bc70a37518dc93f6821209f58728satok                    }
3748898358bfdf4693af02ad454e1deb8034379ce02satok                }
3758898358bfdf4693af02ad454e1deb8034379ce02satok                return spellCheckSpan;
3768898358bfdf4693af02ad454e1deb8034379ce02satok            }
3778898358bfdf4693af02ad454e1deb8034379ce02satok        }
3788898358bfdf4693af02ad454e1deb8034379ce02satok        return null;
3790dc1f648a09b46c45190ba1ce7daecf7fada4347satok    }
3800dc1f648a09b46c45190ba1ce7daecf7fada4347satok
3810dc1f648a09b46c45190ba1ce7daecf7fada4347satok    @Override
3826435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    public void onGetSuggestions(SuggestionsInfo[] results) {
3838898358bfdf4693af02ad454e1deb8034379ce02satok        final Editable editable = (Editable) mTextView.getText();
3848898358bfdf4693af02ad454e1deb8034379ce02satok        for (int i = 0; i < results.length; ++i) {
3858898358bfdf4693af02ad454e1deb8034379ce02satok            final SpellCheckSpan spellCheckSpan =
3868898358bfdf4693af02ad454e1deb8034379ce02satok                    onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE);
3878898358bfdf4693af02ad454e1deb8034379ce02satok            if (spellCheckSpan != null) {
38869865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne                // onSpellCheckSpanRemoved will recycle this span in the pool
3898898358bfdf4693af02ad454e1deb8034379ce02satok                editable.removeSpan(spellCheckSpan);
3908898358bfdf4693af02ad454e1deb8034379ce02satok            }
3918898358bfdf4693af02ad454e1deb8034379ce02satok        }
3928898358bfdf4693af02ad454e1deb8034379ce02satok        scheduleNewSpellCheck();
3938898358bfdf4693af02ad454e1deb8034379ce02satok    }
3948615ac9e049cdf4ab77b0897aca9bceec142c9faGilles Debunne
3958898358bfdf4693af02ad454e1deb8034379ce02satok    @Override
3968898358bfdf4693af02ad454e1deb8034379ce02satok    public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
3978898358bfdf4693af02ad454e1deb8034379ce02satok        final Editable editable = (Editable) mTextView.getText();
3988898358bfdf4693af02ad454e1deb8034379ce02satok
3998898358bfdf4693af02ad454e1deb8034379ce02satok        for (int i = 0; i < results.length; ++i) {
4008898358bfdf4693af02ad454e1deb8034379ce02satok            final SentenceSuggestionsInfo ssi = results[i];
401792ee0cc4d9415e45a16803c6fe3e60c53760e25satok            if (ssi == null) {
402792ee0cc4d9415e45a16803c6fe3e60c53760e25satok                continue;
403792ee0cc4d9415e45a16803c6fe3e60c53760e25satok            }
4048898358bfdf4693af02ad454e1deb8034379ce02satok            SpellCheckSpan spellCheckSpan = null;
4058898358bfdf4693af02ad454e1deb8034379ce02satok            for (int j = 0; j < ssi.getSuggestionsCount(); ++j) {
4068898358bfdf4693af02ad454e1deb8034379ce02satok                final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j);
407792ee0cc4d9415e45a16803c6fe3e60c53760e25satok                if (suggestionsInfo == null) {
408792ee0cc4d9415e45a16803c6fe3e60c53760e25satok                    continue;
409792ee0cc4d9415e45a16803c6fe3e60c53760e25satok                }
4108898358bfdf4693af02ad454e1deb8034379ce02satok                final int offset = ssi.getOffsetAt(j);
4118898358bfdf4693af02ad454e1deb8034379ce02satok                final int length = ssi.getLengthAt(j);
4128898358bfdf4693af02ad454e1deb8034379ce02satok                final SpellCheckSpan scs = onGetSuggestionsInternal(
4138898358bfdf4693af02ad454e1deb8034379ce02satok                        suggestionsInfo, offset, length);
4148898358bfdf4693af02ad454e1deb8034379ce02satok                if (spellCheckSpan == null && scs != null) {
4158898358bfdf4693af02ad454e1deb8034379ce02satok                    // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same
41669865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne                    // SentenceSuggestionsInfo. Removal is deferred after this loop.
4178898358bfdf4693af02ad454e1deb8034379ce02satok                    spellCheckSpan = scs;
4186435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne                }
4196435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne            }
4208898358bfdf4693af02ad454e1deb8034379ce02satok            if (spellCheckSpan != null) {
42169865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne                // onSpellCheckSpanRemoved will recycle this span in the pool
4228898358bfdf4693af02ad454e1deb8034379ce02satok                editable.removeSpan(spellCheckSpan);
4238898358bfdf4693af02ad454e1deb8034379ce02satok            }
4246435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
425be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        scheduleNewSpellCheck();
426be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    }
427be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne
428be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne    private void scheduleNewSpellCheck() {
4298589474d269818713c86ee5e69a685584d1c62e7satok        if (DBG) {
4308589474d269818713c86ee5e69a685584d1c62e7satok            Log.i(TAG, "schedule new spell check.");
4318589474d269818713c86ee5e69a685584d1c62e7satok        }
432be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        if (mSpellRunnable == null) {
433be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            mSpellRunnable = new Runnable() {
434be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                @Override
435be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                public void run() {
436be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                    final int length = mSpellParsers.length;
437be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                    for (int i = 0; i < length; i++) {
438be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                        final SpellParser spellParser = mSpellParsers[i];
439249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne                        if (!spellParser.isFinished()) {
440be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                            spellParser.parse();
441be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                            break; // run one spell parser at a time to bound running time
442be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne                        }
44335199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne                    }
44435199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne                }
445be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            };
446be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        } else {
447be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            mTextView.removeCallbacks(mSpellRunnable);
448287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
449be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne
450be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne        mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
4516435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
4526435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne
4538615ac9e049cdf4ab77b0897aca9bceec142c9faGilles Debunne    private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
4548898358bfdf4693af02ad454e1deb8034379ce02satok            SpellCheckSpan spellCheckSpan, int offset, int length) {
4558898358bfdf4693af02ad454e1deb8034379ce02satok        final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
4568898358bfdf4693af02ad454e1deb8034379ce02satok        final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
4578898358bfdf4693af02ad454e1deb8034379ce02satok        if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart)
4588898358bfdf4693af02ad454e1deb8034379ce02satok            return; // span was removed in the meantime
459176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne
4608898358bfdf4693af02ad454e1deb8034379ce02satok        final int start;
4618898358bfdf4693af02ad454e1deb8034379ce02satok        final int end;
4628898358bfdf4693af02ad454e1deb8034379ce02satok        if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
4638898358bfdf4693af02ad454e1deb8034379ce02satok            start = spellCheckSpanStart + offset;
4648898358bfdf4693af02ad454e1deb8034379ce02satok            end = start + length;
4658898358bfdf4693af02ad454e1deb8034379ce02satok        } else {
4668898358bfdf4693af02ad454e1deb8034379ce02satok            start = spellCheckSpanStart;
4678898358bfdf4693af02ad454e1deb8034379ce02satok            end = spellCheckSpanEnd;
4688898358bfdf4693af02ad454e1deb8034379ce02satok        }
4698898358bfdf4693af02ad454e1deb8034379ce02satok
47041347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne        final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
47141347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne        String[] suggestions;
47241347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne        if (suggestionsCount > 0) {
47341347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne            suggestions = new String[suggestionsCount];
47441347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne            for (int i = 0; i < suggestionsCount; i++) {
47541347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne                suggestions[i] = suggestionsInfo.getSuggestionAt(i);
47641347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne            }
47741347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne        } else {
47841347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne            suggestions = ArrayUtils.emptyArray(String.class);
4796435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne        }
480176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne
481176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne        SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
482176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne                SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
4838589474d269818713c86ee5e69a685584d1c62e7satok        // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
4848589474d269818713c86ee5e69a685584d1c62e7satok        // to share the logic of word level spell checker and sentence level spell checker
4858589474d269818713c86ee5e69a685584d1c62e7satok        if (mIsSentenceSpellCheckSupported) {
48641347e9e8bff93f42ac11a88875ce67e64e5c88cGilles Debunne            final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
4878589474d269818713c86ee5e69a685584d1c62e7satok            final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
4888589474d269818713c86ee5e69a685584d1c62e7satok            if (tempSuggestionSpan != null) {
4898589474d269818713c86ee5e69a685584d1c62e7satok                if (DBG) {
4908589474d269818713c86ee5e69a685584d1c62e7satok                    Log.i(TAG, "Cached span on the same position is cleard. "
4918589474d269818713c86ee5e69a685584d1c62e7satok                            + editable.subSequence(start, end));
4928589474d269818713c86ee5e69a685584d1c62e7satok                }
4938589474d269818713c86ee5e69a685584d1c62e7satok                editable.removeSpan(tempSuggestionSpan);
4948589474d269818713c86ee5e69a685584d1c62e7satok            }
4958589474d269818713c86ee5e69a685584d1c62e7satok            mSuggestionSpanCache.put(key, suggestionSpan);
4968589474d269818713c86ee5e69a685584d1c62e7satok        }
497f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
498176cd0d3b4a612b9fdd88c1d245e9fd93a327bf2Gilles Debunne
499961ebb9ab0a6d45b06a74aa90894f7fda3d528c6Gilles Debunne        mTextView.invalidateRegion(start, end, false /* No cursor involved */);
5006435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne    }
501287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
502287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    private class SpellParser {
503287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        private Object mRange = new Object();
504287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
5050249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne        public void parse(int start, int end) {
50637e169cd1429c76a0223d8fdd77622ead718bbffsatok            final int max = mTextView.length();
50737e169cd1429c76a0223d8fdd77622ead718bbffsatok            final int parseEnd;
50837e169cd1429c76a0223d8fdd77622ead718bbffsatok            if (end > max) {
50937e169cd1429c76a0223d8fdd77622ead718bbffsatok                Log.w(TAG, "Parse invalid region, from " + start + " to " + end);
51037e169cd1429c76a0223d8fdd77622ead718bbffsatok                parseEnd = max;
51137e169cd1429c76a0223d8fdd77622ead718bbffsatok            } else {
51237e169cd1429c76a0223d8fdd77622ead718bbffsatok                parseEnd = end;
51337e169cd1429c76a0223d8fdd77622ead718bbffsatok            }
51437e169cd1429c76a0223d8fdd77622ead718bbffsatok            if (parseEnd > start) {
51537e169cd1429c76a0223d8fdd77622ead718bbffsatok                setRangeSpan((Editable) mTextView.getText(), start, parseEnd);
5160249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne                parse();
5170249b43f6ce59bfec104f0fe606d9059244f8797Gilles Debunne            }
518e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        }
519e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne
520249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        public boolean isFinished() {
521249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
522e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        }
523e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne
524249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne        public void stop() {
525249d1e827a7ffe2a034130d05e18c7b521b0de9bGilles Debunne            removeRangeSpan((Editable) mTextView.getText());
526287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
527287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
528e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        private void setRangeSpan(Editable editable, int start, int end) {
5298589474d269818713c86ee5e69a685584d1c62e7satok            if (DBG) {
5308589474d269818713c86ee5e69a685584d1c62e7satok                Log.d(TAG, "set next range span: " + start + ", " + end);
5318589474d269818713c86ee5e69a685584d1c62e7satok            }
532e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne            editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
533287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
534287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
535e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne        private void removeRangeSpan(Editable editable) {
5368589474d269818713c86ee5e69a685584d1c62e7satok            if (DBG) {
5378589474d269818713c86ee5e69a685584d1c62e7satok                Log.d(TAG, "Remove range span." + editable.getSpanStart(editable)
5388589474d269818713c86ee5e69a685584d1c62e7satok                        + editable.getSpanEnd(editable));
5398589474d269818713c86ee5e69a685584d1c62e7satok            }
540e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne            editable.removeSpan(mRange);
541287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
542287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
543287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        public void parse() {
544f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            Editable editable = (Editable) mTextView.getText();
545287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            // Iterate over the newly added text and schedule new SpellCheckSpans
54624d146b966c87fd9c3b48027cbfb4238cb892ca5satok            final int start;
54724d146b966c87fd9c3b48027cbfb4238cb892ca5satok            if (mIsSentenceSpellCheckSupported) {
54824d146b966c87fd9c3b48027cbfb4238cb892ca5satok                // TODO: Find the start position of the sentence.
54924d146b966c87fd9c3b48027cbfb4238cb892ca5satok                // Set span with the context
55024d146b966c87fd9c3b48027cbfb4238cb892ca5satok                start =  Math.max(
55124d146b966c87fd9c3b48027cbfb4238cb892ca5satok                        0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH);
55224d146b966c87fd9c3b48027cbfb4238cb892ca5satok            } else {
55324d146b966c87fd9c3b48027cbfb4238cb892ca5satok                start = editable.getSpanStart(mRange);
55424d146b966c87fd9c3b48027cbfb4238cb892ca5satok            }
55524d146b966c87fd9c3b48027cbfb4238cb892ca5satok
556f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            final int end = editable.getSpanEnd(mRange);
55735199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne
55835199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne            int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
559be5f49fb6e17e0b9588d3b94022b7e3eb6d47317Gilles Debunne            mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
560287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
561287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            // Move back to the beginning of the current word, if any
562287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            int wordStart = mWordIterator.preceding(start);
563287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            int wordEnd;
564287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            if (wordStart == BreakIterator.DONE) {
565287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                wordEnd = mWordIterator.following(start);
566287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                if (wordEnd != BreakIterator.DONE) {
567287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                    wordStart = mWordIterator.getBeginning(wordEnd);
568287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                }
569287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            } else {
570287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                wordEnd = mWordIterator.getEnd(wordStart);
571287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
572287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            if (wordEnd == BreakIterator.DONE) {
5738589474d269818713c86ee5e69a685584d1c62e7satok                if (DBG) {
5748589474d269818713c86ee5e69a685584d1c62e7satok                    Log.i(TAG, "No more spell check.");
5758589474d269818713c86ee5e69a685584d1c62e7satok                }
576e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne                removeRangeSpan(editable);
577287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                return;
578287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
579287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
580287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            // We need to expand by one character because we want to include the spans that
581287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            // end/start at position start/end respectively.
582f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
583f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                    SpellCheckSpan.class);
584f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne            SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
585f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                    SuggestionSpan.class);
586287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
58735199f5ce7cfc433628a1beda84d80fcaa475e41Gilles Debunne            int wordCount = 0;
588287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            boolean scheduleOtherSpellCheck = false;
589287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
5908898358bfdf4693af02ad454e1deb8034379ce02satok            if (mIsSentenceSpellCheckSupported) {
5918898358bfdf4693af02ad454e1deb8034379ce02satok                if (wordIteratorWindowEnd < end) {
5928589474d269818713c86ee5e69a685584d1c62e7satok                    if (DBG) {
5938589474d269818713c86ee5e69a685584d1c62e7satok                        Log.i(TAG, "schedule other spell check.");
5948589474d269818713c86ee5e69a685584d1c62e7satok                    }
5958898358bfdf4693af02ad454e1deb8034379ce02satok                    // Several batches needed on that region. Cut after last previous word
5968898358bfdf4693af02ad454e1deb8034379ce02satok                    scheduleOtherSpellCheck = true;
5978898358bfdf4693af02ad454e1deb8034379ce02satok                }
5988589474d269818713c86ee5e69a685584d1c62e7satok                int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd);
5998589474d269818713c86ee5e69a685584d1c62e7satok                boolean correct = spellCheckEnd != BreakIterator.DONE;
6008898358bfdf4693af02ad454e1deb8034379ce02satok                if (correct) {
6018589474d269818713c86ee5e69a685584d1c62e7satok                    spellCheckEnd = mWordIterator.getEnd(spellCheckEnd);
6028589474d269818713c86ee5e69a685584d1c62e7satok                    correct = spellCheckEnd != BreakIterator.DONE;
6038898358bfdf4693af02ad454e1deb8034379ce02satok                }
6048898358bfdf4693af02ad454e1deb8034379ce02satok                if (!correct) {
6058589474d269818713c86ee5e69a685584d1c62e7satok                    if (DBG) {
6068589474d269818713c86ee5e69a685584d1c62e7satok                        Log.i(TAG, "Incorrect range span.");
6078589474d269818713c86ee5e69a685584d1c62e7satok                    }
6088589474d269818713c86ee5e69a685584d1c62e7satok                    removeRangeSpan(editable);
6098898358bfdf4693af02ad454e1deb8034379ce02satok                    return;
6108898358bfdf4693af02ad454e1deb8034379ce02satok                }
6118589474d269818713c86ee5e69a685584d1c62e7satok                do {
6128589474d269818713c86ee5e69a685584d1c62e7satok                    // TODO: Find the start position of the sentence.
6138589474d269818713c86ee5e69a685584d1c62e7satok                    int spellCheckStart = wordStart;
6148589474d269818713c86ee5e69a685584d1c62e7satok                    boolean createSpellCheckSpan = true;
6158589474d269818713c86ee5e69a685584d1c62e7satok                    // Cancel or merge overlapped spell check spans
6168589474d269818713c86ee5e69a685584d1c62e7satok                    for (int i = 0; i < mLength; ++i) {
6178589474d269818713c86ee5e69a685584d1c62e7satok                        final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
6188589474d269818713c86ee5e69a685584d1c62e7satok                        if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) {
6198589474d269818713c86ee5e69a685584d1c62e7satok                            continue;
6208589474d269818713c86ee5e69a685584d1c62e7satok                        }
6218589474d269818713c86ee5e69a685584d1c62e7satok                        final int spanStart = editable.getSpanStart(spellCheckSpan);
6228589474d269818713c86ee5e69a685584d1c62e7satok                        final int spanEnd = editable.getSpanEnd(spellCheckSpan);
6238589474d269818713c86ee5e69a685584d1c62e7satok                        if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) {
6248589474d269818713c86ee5e69a685584d1c62e7satok                            // No need to merge
6258589474d269818713c86ee5e69a685584d1c62e7satok                            continue;
6268589474d269818713c86ee5e69a685584d1c62e7satok                        }
6278589474d269818713c86ee5e69a685584d1c62e7satok                        if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) {
6288589474d269818713c86ee5e69a685584d1c62e7satok                            // There is a completely overlapped spell check span
6298589474d269818713c86ee5e69a685584d1c62e7satok                            // skip this span
6308589474d269818713c86ee5e69a685584d1c62e7satok                            createSpellCheckSpan = false;
6318589474d269818713c86ee5e69a685584d1c62e7satok                            if (DBG) {
6328589474d269818713c86ee5e69a685584d1c62e7satok                                Log.i(TAG, "The range is overrapped. Skip spell check.");
6338589474d269818713c86ee5e69a685584d1c62e7satok                            }
6348589474d269818713c86ee5e69a685584d1c62e7satok                            break;
6358589474d269818713c86ee5e69a685584d1c62e7satok                        }
63669865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne                        // This spellCheckSpan is replaced by the one we are creating
63769865bd6860a97793a06523a48dfe6472e9b7562Gilles Debunne                        editable.removeSpan(spellCheckSpan);
6388589474d269818713c86ee5e69a685584d1c62e7satok                        spellCheckStart = Math.min(spanStart, spellCheckStart);
6398589474d269818713c86ee5e69a685584d1c62e7satok                        spellCheckEnd = Math.max(spanEnd, spellCheckEnd);
6408589474d269818713c86ee5e69a685584d1c62e7satok                    }
6418589474d269818713c86ee5e69a685584d1c62e7satok
6428589474d269818713c86ee5e69a685584d1c62e7satok                    if (DBG) {
6438589474d269818713c86ee5e69a685584d1c62e7satok                        Log.d(TAG, "addSpellCheckSpan: "
6448589474d269818713c86ee5e69a685584d1c62e7satok                                + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart
6458589474d269818713c86ee5e69a685584d1c62e7satok                                + ", next = " + scheduleOtherSpellCheck + "\n"
6468589474d269818713c86ee5e69a685584d1c62e7satok                                + editable.subSequence(spellCheckStart, spellCheckEnd));
6478589474d269818713c86ee5e69a685584d1c62e7satok                    }
6488589474d269818713c86ee5e69a685584d1c62e7satok
6498589474d269818713c86ee5e69a685584d1c62e7satok                    // Stop spell checking when there are no characters in the range.
6508589474d269818713c86ee5e69a685584d1c62e7satok                    if (spellCheckEnd < start) {
6518589474d269818713c86ee5e69a685584d1c62e7satok                        break;
6528589474d269818713c86ee5e69a685584d1c62e7satok                    }
653a4c82c1c78cdd37e0dea1b5b2f44c25cc584034csatok                    if (spellCheckEnd <= spellCheckStart) {
65437e169cd1429c76a0223d8fdd77622ead718bbffsatok                        Log.w(TAG, "Trying to spellcheck invalid region, from "
65537e169cd1429c76a0223d8fdd77622ead718bbffsatok                                + start + " to " + end);
656a4c82c1c78cdd37e0dea1b5b2f44c25cc584034csatok                        break;
657a4c82c1c78cdd37e0dea1b5b2f44c25cc584034csatok                    }
6588589474d269818713c86ee5e69a685584d1c62e7satok                    if (createSpellCheckSpan) {
6598589474d269818713c86ee5e69a685584d1c62e7satok                        addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd);
6608589474d269818713c86ee5e69a685584d1c62e7satok                    }
6618589474d269818713c86ee5e69a685584d1c62e7satok                } while (false);
6628589474d269818713c86ee5e69a685584d1c62e7satok                wordStart = spellCheckEnd;
6638898358bfdf4693af02ad454e1deb8034379ce02satok            } else {
6648898358bfdf4693af02ad454e1deb8034379ce02satok                while (wordStart <= end) {
6658898358bfdf4693af02ad454e1deb8034379ce02satok                    if (wordEnd >= start && wordEnd > wordStart) {
6668898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordCount >= MAX_NUMBER_OF_WORDS) {
6678898358bfdf4693af02ad454e1deb8034379ce02satok                            scheduleOtherSpellCheck = true;
6688898358bfdf4693af02ad454e1deb8034379ce02satok                            break;
6698898358bfdf4693af02ad454e1deb8034379ce02satok                        }
6708898358bfdf4693af02ad454e1deb8034379ce02satok                        // A new word has been created across the interval boundaries with this
6718898358bfdf4693af02ad454e1deb8034379ce02satok                        // edit. The previous spans (that ended on start / started on end) are
6728898358bfdf4693af02ad454e1deb8034379ce02satok                        // not valid anymore and must be removed.
6738898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordStart < start && wordEnd > start) {
6748898358bfdf4693af02ad454e1deb8034379ce02satok                            removeSpansAt(editable, start, spellCheckSpans);
6758898358bfdf4693af02ad454e1deb8034379ce02satok                            removeSpansAt(editable, start, suggestionSpans);
6768898358bfdf4693af02ad454e1deb8034379ce02satok                        }
677287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
6788898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordStart < end && wordEnd > end) {
6798898358bfdf4693af02ad454e1deb8034379ce02satok                            removeSpansAt(editable, end, spellCheckSpans);
6808898358bfdf4693af02ad454e1deb8034379ce02satok                            removeSpansAt(editable, end, suggestionSpans);
6818898358bfdf4693af02ad454e1deb8034379ce02satok                        }
682287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
6838898358bfdf4693af02ad454e1deb8034379ce02satok                        // Do not create new boundary spans if they already exist
6848898358bfdf4693af02ad454e1deb8034379ce02satok                        boolean createSpellCheckSpan = true;
6858898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordEnd == start) {
6868898358bfdf4693af02ad454e1deb8034379ce02satok                            for (int i = 0; i < spellCheckSpans.length; i++) {
6878898358bfdf4693af02ad454e1deb8034379ce02satok                                final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
6888898358bfdf4693af02ad454e1deb8034379ce02satok                                if (spanEnd == start) {
6898898358bfdf4693af02ad454e1deb8034379ce02satok                                    createSpellCheckSpan = false;
6908898358bfdf4693af02ad454e1deb8034379ce02satok                                    break;
6918898358bfdf4693af02ad454e1deb8034379ce02satok                                }
692287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                            }
693287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                        }
694287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
6958898358bfdf4693af02ad454e1deb8034379ce02satok                        if (wordStart == end) {
6968898358bfdf4693af02ad454e1deb8034379ce02satok                            for (int i = 0; i < spellCheckSpans.length; i++) {
6978898358bfdf4693af02ad454e1deb8034379ce02satok                                final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
6988898358bfdf4693af02ad454e1deb8034379ce02satok                                if (spanStart == end) {
6998898358bfdf4693af02ad454e1deb8034379ce02satok                                    createSpellCheckSpan = false;
7008898358bfdf4693af02ad454e1deb8034379ce02satok                                    break;
7018898358bfdf4693af02ad454e1deb8034379ce02satok                                }
702287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                            }
703287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                        }
704287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
7058898358bfdf4693af02ad454e1deb8034379ce02satok                        if (createSpellCheckSpan) {
7068898358bfdf4693af02ad454e1deb8034379ce02satok                            addSpellCheckSpan(editable, wordStart, wordEnd);
7078898358bfdf4693af02ad454e1deb8034379ce02satok                        }
7088898358bfdf4693af02ad454e1deb8034379ce02satok                        wordCount++;
709287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                    }
710287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
7118898358bfdf4693af02ad454e1deb8034379ce02satok                    // iterate word by word
7128898358bfdf4693af02ad454e1deb8034379ce02satok                    int originalWordEnd = wordEnd;
7138898358bfdf4693af02ad454e1deb8034379ce02satok                    wordEnd = mWordIterator.following(wordEnd);
7148898358bfdf4693af02ad454e1deb8034379ce02satok                    if ((wordIteratorWindowEnd < end) &&
7158898358bfdf4693af02ad454e1deb8034379ce02satok                            (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
7168898358bfdf4693af02ad454e1deb8034379ce02satok                        wordIteratorWindowEnd =
7178898358bfdf4693af02ad454e1deb8034379ce02satok                                Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
7188898358bfdf4693af02ad454e1deb8034379ce02satok                        mWordIterator.setCharSequence(
7198898358bfdf4693af02ad454e1deb8034379ce02satok                                editable, originalWordEnd, wordIteratorWindowEnd);
7208898358bfdf4693af02ad454e1deb8034379ce02satok                        wordEnd = mWordIterator.following(originalWordEnd);
7218898358bfdf4693af02ad454e1deb8034379ce02satok                    }
7228898358bfdf4693af02ad454e1deb8034379ce02satok                    if (wordEnd == BreakIterator.DONE) break;
7238898358bfdf4693af02ad454e1deb8034379ce02satok                    wordStart = mWordIterator.getBeginning(wordEnd);
7248898358bfdf4693af02ad454e1deb8034379ce02satok                    if (wordStart == BreakIterator.DONE) {
7258898358bfdf4693af02ad454e1deb8034379ce02satok                        break;
7268898358bfdf4693af02ad454e1deb8034379ce02satok                    }
727287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                }
728287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
729287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
7304cdeeadd42bc11f6f5f60f5301c09025cabfc65fRoozbeh Pournader            if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) {
731e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne                // Update range span: start new spell check from last wordStart
732e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne                setRangeSpan(editable, wordStart, end);
733287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            } else {
734e9b82808d412fa1b87954dd88579b92d0b4ab0e2Gilles Debunne                removeRangeSpan(editable);
735287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
736287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
737287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            spellCheck();
738287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
739287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne
740f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne        private <T> void removeSpansAt(Editable editable, int offset, T[] spans) {
741287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            final int length = spans.length;
742287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            for (int i = 0; i < length; i++) {
743287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                final T span = spans[i];
744f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                final int start = editable.getSpanStart(span);
745287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                if (start > offset) continue;
746f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                final int end = editable.getSpanEnd(span);
747287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne                if (end < offset) continue;
748f656030a865be029c7c29bab2ea6f5fff07624aeGilles Debunne                editable.removeSpan(span);
749287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne            }
750287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne        }
751287d6c6e12a38864d019fa7b9184206bc8a31ea1Gilles Debunne    }
752d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka
753d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka    public static boolean haveWordBoundariesChanged(final Editable editable, final int start,
754d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            final int end, final int spanStart, final int spanEnd) {
755d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka        final boolean haveWordBoundariesChanged;
756d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka        if (spanEnd != start && spanStart != end) {
757d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            haveWordBoundariesChanged = true;
758d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            if (DBG) {
759d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                Log.d(TAG, "(1) Text inside the span has been modified. Remove.");
760d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            }
761d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka        } else if (spanEnd == start && start < editable.length()) {
762d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            final int codePoint = Character.codePointAt(editable, start);
763d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
764d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            if (DBG) {
765d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                Log.d(TAG, "(2) Characters have been appended to the spanned text. "
766d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                        + (haveWordBoundariesChanged ? "Remove.<" : "Keep. <") + (char)(codePoint)
767d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                        + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
768d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                        + start);
769d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            }
770d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka        } else if (spanStart == end && end > 0) {
771d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            final int codePoint = Character.codePointBefore(editable, end);
772d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
773d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            if (DBG) {
774d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                Log.d(TAG, "(3) Characters have been prepended to the spanned text. "
775d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                        + (haveWordBoundariesChanged ? "Remove.<" : "Keep.<") + (char)(codePoint)
776d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                        + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
777d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                        + end);
778d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            }
779d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka        } else {
780d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            if (DBG) {
781d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka                Log.d(TAG, "(4) Characters adjacent to the spanned text were deleted. Keep.");
782d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            }
783d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka            haveWordBoundariesChanged = false;
784d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka        }
785d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka        return haveWordBoundariesChanged;
786d7429c15e6f4dd9a43b81d94a1bbf65b17d46a16Satoshi Kataoka    }
7876435a56a8c02de98befcc8cd743b2b638cffb327Gilles Debunne}
788