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