blob: a10615956a2067c6270696f9d2c11cbde9904bef [file] [log] [blame]
Gilles Debunne0eea6682011-08-29 13:30:31 -07001/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Gilles Debunne6435a562011-08-04 21:22:30 -070016
17package android.widget;
18
19import android.content.Context;
20import android.text.Editable;
21import android.text.Selection;
Gilles Debunne653d3a22011-12-07 10:35:59 -080022import android.text.SpannableStringBuilder;
Gilles Debunne6435a562011-08-04 21:22:30 -070023import android.text.Spanned;
Gilles Debunne287d6c62011-10-05 18:22:11 -070024import android.text.method.WordIterator;
Gilles Debunne6435a562011-08-04 21:22:30 -070025import android.text.style.SpellCheckSpan;
26import android.text.style.SuggestionSpan;
Gilles Debunne6435a562011-08-04 21:22:30 -070027import android.view.textservice.SpellCheckerSession;
28import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
29import android.view.textservice.SuggestionsInfo;
30import android.view.textservice.TextInfo;
31import android.view.textservice.TextServicesManager;
32
33import com.android.internal.util.ArrayUtils;
34
Gilles Debunne287d6c62011-10-05 18:22:11 -070035import java.text.BreakIterator;
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070036import java.util.Locale;
Gilles Debunne287d6c62011-10-05 18:22:11 -070037
Gilles Debunne6435a562011-08-04 21:22:30 -070038
39/**
40 * Helper class for TextView. Bridge between the TextView and the Dictionnary service.
41 *
42 * @hide
43 */
44public class SpellChecker implements SpellCheckerSessionListener {
Gilles Debunne6435a562011-08-04 21:22:30 -070045
Gilles Debunne35199f52011-10-25 15:05:16 -070046 // No more than this number of words will be parsed on each iteration to ensure a minimum
47 // lock of the UI thread
Gilles Debunnebe5f49f2011-10-25 15:05:16 -070048 public static final int MAX_NUMBER_OF_WORDS = 50;
Gilles Debunne35199f52011-10-25 15:05:16 -070049
Gilles Debunnebe5f49f2011-10-25 15:05:16 -070050 // Rough estimate, such that the word iterator interval usually does not need to be shifted
51 public static final int AVERAGE_WORD_LENGTH = 7;
Gilles Debunne35199f52011-10-25 15:05:16 -070052
53 // When parsing, use a character window of that size. Will be shifted if needed
54 public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS;
55
Gilles Debunnebe5f49f2011-10-25 15:05:16 -070056 // Pause between each spell check to keep the UI smooth
Gilles Debunne35199f52011-10-25 15:05:16 -070057 private final static int SPELL_PAUSE_DURATION = 400; // milliseconds
Gilles Debunne287d6c62011-10-05 18:22:11 -070058
Gilles Debunne6435a562011-08-04 21:22:30 -070059 private final TextView mTextView;
60
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070061 SpellCheckerSession mSpellCheckerSession;
Gilles Debunne6435a562011-08-04 21:22:30 -070062 final int mCookie;
63
Gilles Debunneb062e812011-09-27 14:58:37 -070064 // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated
65 // SpellCheckSpan has been recycled and can be-reused.
Gilles Debunne287d6c62011-10-05 18:22:11 -070066 // Contains null SpellCheckSpans after index mLength.
Gilles Debunne6435a562011-08-04 21:22:30 -070067 private int[] mIds;
68 private SpellCheckSpan[] mSpellCheckSpans;
Gilles Debunneb062e812011-09-27 14:58:37 -070069 // The mLength first elements of the above arrays have been initialized
Gilles Debunne6435a562011-08-04 21:22:30 -070070 private int mLength;
71
Gilles Debunne287d6c62011-10-05 18:22:11 -070072 // Parsers on chunck of text, cutting text into words that will be checked
73 private SpellParser[] mSpellParsers = new SpellParser[0];
74
Gilles Debunne6435a562011-08-04 21:22:30 -070075 private int mSpanSequenceCounter = 0;
Gilles Debunne6435a562011-08-04 21:22:30 -070076
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070077 private Locale mCurrentLocale;
78
79 // Shared by all SpellParsers. Cannot be shared with TextView since it may be used
80 // concurrently due to the asynchronous nature of onGetSuggestions.
81 private WordIterator mWordIterator;
82
Gilles Debunnea49ba2f2011-12-01 17:41:15 -080083 private TextServicesManager mTextServicesManager;
84
Gilles Debunnebe5f49f2011-10-25 15:05:16 -070085 private Runnable mSpellRunnable;
86
Gilles Debunne6435a562011-08-04 21:22:30 -070087 public SpellChecker(TextView textView) {
88 mTextView = textView;
89
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070090 // Arbitrary: these arrays will automatically double their sizes on demand
Gilles Debunneb062e812011-09-27 14:58:37 -070091 final int size = ArrayUtils.idealObjectArraySize(1);
Gilles Debunne6435a562011-08-04 21:22:30 -070092 mIds = new int[size];
93 mSpellCheckSpans = new SpellCheckSpan[size];
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070094
satok05f24702011-11-02 19:29:35 +090095 setLocale(mTextView.getTextServicesLocale());
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070096
97 mCookie = hashCode();
98 }
99
Gilles Debunne249d1e82011-12-12 20:06:29 -0800100 private void resetSession() {
Marco Nelissen56735442011-11-09 16:07:33 -0800101 closeSession();
Gilles Debunne249d1e82011-12-12 20:06:29 -0800102
103 mTextServicesManager = (TextServicesManager) mTextView.getContext().
104 getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
105 if (!mTextServicesManager.isSpellCheckerEnabled()) {
satok9b3855b2011-11-02 17:01:28 +0900106 mSpellCheckerSession = null;
107 } else {
Gilles Debunne249d1e82011-12-12 20:06:29 -0800108 mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
satok9b3855b2011-11-02 17:01:28 +0900109 null /* Bundle not currently used by the textServicesManager */,
Gilles Debunne249d1e82011-12-12 20:06:29 -0800110 mCurrentLocale, this,
satok9b3855b2011-11-02 17:01:28 +0900111 false /* means any available languages from current spell checker */);
112 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700113
114 // Restore SpellCheckSpans in pool
115 for (int i = 0; i < mLength; i++) {
116 mSpellCheckSpans[i].setSpellCheckInProgress(false);
117 mIds[i] = -1;
118 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700119 mLength = 0;
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700120
Gilles Debunne249d1e82011-12-12 20:06:29 -0800121 // Remove existing misspelled SuggestionSpans
122 mTextView.removeMisspelledSpans((Editable) mTextView.getText());
123 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700124
Gilles Debunne249d1e82011-12-12 20:06:29 -0800125 private void setLocale(Locale locale) {
126 mCurrentLocale = locale;
127
128 resetSession();
129
130 // Change SpellParsers' wordIterator locale
131 mWordIterator = new WordIterator(locale);
132
133 // This class is the listener for locale change: warn other locale-aware objects
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700134 mTextView.onLocaleChanged();
Gilles Debunne6435a562011-08-04 21:22:30 -0700135 }
136
Gilles Debunne186aaf92011-09-16 14:26:12 -0700137 /**
138 * @return true if a spell checker session has successfully been created. Returns false if not,
139 * for instance when spell checking has been disabled in settings.
140 */
Gilles Debunne287d6c62011-10-05 18:22:11 -0700141 private boolean isSessionActive() {
Gilles Debunne186aaf92011-09-16 14:26:12 -0700142 return mSpellCheckerSession != null;
143 }
144
145 public void closeSession() {
146 if (mSpellCheckerSession != null) {
147 mSpellCheckerSession.close();
148 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700149
150 final int length = mSpellParsers.length;
151 for (int i = 0; i < length; i++) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700152 mSpellParsers[i].stop();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700153 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700154
155 if (mSpellRunnable != null) {
156 mTextView.removeCallbacks(mSpellRunnable);
157 }
Gilles Debunne186aaf92011-09-16 14:26:12 -0700158 }
159
Gilles Debunneb062e812011-09-27 14:58:37 -0700160 private int nextSpellCheckSpanIndex() {
161 for (int i = 0; i < mLength; i++) {
162 if (mIds[i] < 0) return i;
163 }
164
165 if (mLength == mSpellCheckSpans.length) {
166 final int newSize = mLength * 2;
Gilles Debunne6435a562011-08-04 21:22:30 -0700167 int[] newIds = new int[newSize];
168 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize];
Gilles Debunneb062e812011-09-27 14:58:37 -0700169 System.arraycopy(mIds, 0, newIds, 0, mLength);
170 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength);
Gilles Debunne6435a562011-08-04 21:22:30 -0700171 mIds = newIds;
172 mSpellCheckSpans = newSpellCheckSpans;
173 }
174
Gilles Debunneb062e812011-09-27 14:58:37 -0700175 mSpellCheckSpans[mLength] = new SpellCheckSpan();
Gilles Debunne6435a562011-08-04 21:22:30 -0700176 mLength++;
Gilles Debunneb062e812011-09-27 14:58:37 -0700177 return mLength - 1;
178 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700179
Gilles Debunnef6560302011-10-10 15:03:55 -0700180 private void addSpellCheckSpan(Editable editable, int start, int end) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700181 final int index = nextSpellCheckSpanIndex();
Gilles Debunnef6560302011-10-10 15:03:55 -0700182 editable.setSpan(mSpellCheckSpans[index], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunneb062e812011-09-27 14:58:37 -0700183 mIds[index] = mSpanSequenceCounter++;
Gilles Debunne6435a562011-08-04 21:22:30 -0700184 }
185
186 public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) {
187 for (int i = 0; i < mLength; i++) {
188 if (mSpellCheckSpans[i] == spellCheckSpan) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700189 mSpellCheckSpans[i].setSpellCheckInProgress(false);
190 mIds[i] = -1;
Gilles Debunne6435a562011-08-04 21:22:30 -0700191 return;
192 }
193 }
194 }
195
Gilles Debunne6435a562011-08-04 21:22:30 -0700196 public void onSelectionChanged() {
Gilles Debunneb062e812011-09-27 14:58:37 -0700197 spellCheck();
Gilles Debunne6435a562011-08-04 21:22:30 -0700198 }
199
Gilles Debunne287d6c62011-10-05 18:22:11 -0700200 public void spellCheck(int start, int end) {
satok05f24702011-11-02 19:29:35 +0900201 final Locale locale = mTextView.getTextServicesLocale();
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700202 if (mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
203 setLocale(locale);
204 // Re-check the entire text
205 start = 0;
206 end = mTextView.getText().length();
Gilles Debunne249d1e82011-12-12 20:06:29 -0800207 } else {
208 final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
209 if (isSessionActive() != spellCheckerActivated) {
210 // Spell checker has been turned of or off since last spellCheck
211 resetSession();
212 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700213 }
214
Gilles Debunne287d6c62011-10-05 18:22:11 -0700215 if (!isSessionActive()) return;
216
Gilles Debunnee9b82802011-10-27 14:38:27 -0700217 // Find first available SpellParser from pool
Gilles Debunne287d6c62011-10-05 18:22:11 -0700218 final int length = mSpellParsers.length;
219 for (int i = 0; i < length; i++) {
220 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunne249d1e82011-12-12 20:06:29 -0800221 if (spellParser.isFinished()) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700222 spellParser.init(start, end);
223 spellParser.parse();
224 return;
225 }
226 }
227
228 // No available parser found in pool, create a new one
229 SpellParser[] newSpellParsers = new SpellParser[length + 1];
230 System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
231 mSpellParsers = newSpellParsers;
232
233 SpellParser spellParser = new SpellParser();
234 mSpellParsers[length] = spellParser;
235 spellParser.init(start, end);
236 spellParser.parse();
237 }
238
239 private void spellCheck() {
Gilles Debunne0eea6682011-08-29 13:30:31 -0700240 if (mSpellCheckerSession == null) return;
Gilles Debunne99068472011-08-29 12:05:11 -0700241
Gilles Debunnef6560302011-10-10 15:03:55 -0700242 Editable editable = (Editable) mTextView.getText();
243 final int selectionStart = Selection.getSelectionStart(editable);
244 final int selectionEnd = Selection.getSelectionEnd(editable);
Gilles Debunne6435a562011-08-04 21:22:30 -0700245
246 TextInfo[] textInfos = new TextInfo[mLength];
247 int textInfosCount = 0;
248
249 for (int i = 0; i < mLength; i++) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700250 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
Gilles Debunne6435a562011-08-04 21:22:30 -0700251 if (spellCheckSpan.isSpellCheckInProgress()) continue;
252
Gilles Debunnef6560302011-10-10 15:03:55 -0700253 final int start = editable.getSpanStart(spellCheckSpan);
254 final int end = editable.getSpanEnd(spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700255
256 // Do not check this word if the user is currently editing it
Gilles Debunned6e34942011-08-26 10:13:15 -0700257 if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) {
Gilles Debunne653d3a22011-12-07 10:35:59 -0800258 final String word = (editable instanceof SpannableStringBuilder) ?
259 ((SpannableStringBuilder) editable).substring(start, end) :
260 editable.subSequence(start, end).toString();
Gilles Debunneb062e812011-09-27 14:58:37 -0700261 spellCheckSpan.setSpellCheckInProgress(true);
Gilles Debunne6435a562011-08-04 21:22:30 -0700262 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
263 }
264 }
265
266 if (textInfosCount > 0) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700267 if (textInfosCount < textInfos.length) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700268 TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
269 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
270 textInfos = textInfosCopy;
271 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700272
Gilles Debunne0eea6682011-08-29 13:30:31 -0700273 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
Gilles Debunne6435a562011-08-04 21:22:30 -0700274 false /* TODO Set sequentialWords to true for initial spell check */);
275 }
276 }
277
278 @Override
satok0dc1f642011-11-18 11:27:10 +0900279 public void onGetSuggestionsForSentence(SuggestionsInfo[] results) {
280 // TODO: Handle the position and length for each suggestion
281 onGetSuggestions(results);
282 }
283
284 @Override
Gilles Debunne6435a562011-08-04 21:22:30 -0700285 public void onGetSuggestions(SuggestionsInfo[] results) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700286 Editable editable = (Editable) mTextView.getText();
287
Gilles Debunne6435a562011-08-04 21:22:30 -0700288 for (int i = 0; i < results.length; i++) {
289 SuggestionsInfo suggestionsInfo = results[i];
290 if (suggestionsInfo.getCookie() != mCookie) continue;
Gilles Debunne6435a562011-08-04 21:22:30 -0700291 final int sequenceNumber = suggestionsInfo.getSequence();
Gilles Debunneb062e812011-09-27 14:58:37 -0700292
293 for (int j = 0; j < mLength; j++) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700294 if (sequenceNumber == mIds[j]) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700295 final int attributes = suggestionsInfo.getSuggestionsAttributes();
296 boolean isInDictionary =
297 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
298 boolean looksLikeTypo =
299 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
300
Gilles Debunnee1fc4f62011-10-03 17:01:19 -0700301 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
Gilles Debunne8615ac92011-11-29 15:25:03 -0800302
Gilles Debunne6435a562011-08-04 21:22:30 -0700303 if (!isInDictionary && looksLikeTypo) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700304 createMisspelledSuggestionSpan(editable, suggestionsInfo, spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700305 }
Gilles Debunne8615ac92011-11-29 15:25:03 -0800306
Gilles Debunnef6560302011-10-10 15:03:55 -0700307 editable.removeSpan(spellCheckSpan);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700308 break;
Gilles Debunne6435a562011-08-04 21:22:30 -0700309 }
310 }
311 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700312
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700313 scheduleNewSpellCheck();
314 }
315
316 private void scheduleNewSpellCheck() {
317 if (mSpellRunnable == null) {
318 mSpellRunnable = new Runnable() {
319 @Override
320 public void run() {
321 final int length = mSpellParsers.length;
322 for (int i = 0; i < length; i++) {
323 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunne249d1e82011-12-12 20:06:29 -0800324 if (!spellParser.isFinished()) {
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700325 spellParser.parse();
326 break; // run one spell parser at a time to bound running time
327 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700328 }
329 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700330 };
331 } else {
332 mTextView.removeCallbacks(mSpellRunnable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700333 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700334
335 mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
Gilles Debunne6435a562011-08-04 21:22:30 -0700336 }
337
Gilles Debunne8615ac92011-11-29 15:25:03 -0800338 private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
339 SpellCheckSpan spellCheckSpan) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700340 final int start = editable.getSpanStart(spellCheckSpan);
341 final int end = editable.getSpanEnd(spellCheckSpan);
Gilles Debunne8615ac92011-11-29 15:25:03 -0800342 if (start < 0 || end <= start) return; // span was removed in the meantime
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700343
344 // Other suggestion spans may exist on that region, with identical suggestions, filter
Gilles Debunne8615ac92011-11-29 15:25:03 -0800345 // them out to avoid duplicates.
Gilles Debunnef6560302011-10-10 15:03:55 -0700346 SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700347 final int length = suggestionSpans.length;
348 for (int i = 0; i < length; i++) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700349 final int spanStart = editable.getSpanStart(suggestionSpans[i]);
350 final int spanEnd = editable.getSpanEnd(suggestionSpans[i]);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700351 if (spanStart != start || spanEnd != end) {
Gilles Debunne8615ac92011-11-29 15:25:03 -0800352 // Nulled (to avoid new array allocation) if not on that exact same region
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700353 suggestionSpans[i] = null;
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700354 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700355 }
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700356
357 final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
358 String[] suggestions;
359 if (suggestionsCount <= 0) {
360 // A negative suggestion count is possible
361 suggestions = ArrayUtils.emptyArray(String.class);
362 } else {
363 int numberOfSuggestions = 0;
364 suggestions = new String[suggestionsCount];
365
366 for (int i = 0; i < suggestionsCount; i++) {
367 final String spellSuggestion = suggestionsInfo.getSuggestionAt(i);
368 if (spellSuggestion == null) break;
369 boolean suggestionFound = false;
370
371 for (int j = 0; j < length && !suggestionFound; j++) {
372 if (suggestionSpans[j] == null) break;
373
374 String[] suggests = suggestionSpans[j].getSuggestions();
375 for (int k = 0; k < suggests.length; k++) {
376 if (spellSuggestion.equals(suggests[k])) {
377 // The suggestion is already provided by an other SuggestionSpan
378 suggestionFound = true;
379 break;
380 }
381 }
382 }
383
384 if (!suggestionFound) {
385 suggestions[numberOfSuggestions++] = spellSuggestion;
386 }
387 }
388
389 if (numberOfSuggestions != suggestionsCount) {
390 String[] newSuggestions = new String[numberOfSuggestions];
391 System.arraycopy(suggestions, 0, newSuggestions, 0, numberOfSuggestions);
392 suggestions = newSuggestions;
393 }
394 }
395
396 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
397 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
Gilles Debunnef6560302011-10-10 15:03:55 -0700398 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700399
Gilles Debunne961ebb92011-12-12 10:16:04 -0800400 mTextView.invalidateRegion(start, end, false /* No cursor involved */);
Gilles Debunne6435a562011-08-04 21:22:30 -0700401 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700402
403 private class SpellParser {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700404 private Object mRange = new Object();
405
406 public void init(int start, int end) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700407 setRangeSpan((Editable) mTextView.getText(), start, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700408 }
409
Gilles Debunne249d1e82011-12-12 20:06:29 -0800410 public boolean isFinished() {
411 return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700412 }
413
Gilles Debunne249d1e82011-12-12 20:06:29 -0800414 public void stop() {
415 removeRangeSpan((Editable) mTextView.getText());
Gilles Debunnee9b82802011-10-27 14:38:27 -0700416 }
417
418 private void setRangeSpan(Editable editable, int start, int end) {
419 editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
420 }
421
422 private void removeRangeSpan(Editable editable) {
423 editable.removeSpan(mRange);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700424 }
425
426 public void parse() {
Gilles Debunnef6560302011-10-10 15:03:55 -0700427 Editable editable = (Editable) mTextView.getText();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700428 // Iterate over the newly added text and schedule new SpellCheckSpans
Gilles Debunnef6560302011-10-10 15:03:55 -0700429 final int start = editable.getSpanStart(mRange);
430 final int end = editable.getSpanEnd(mRange);
Gilles Debunne35199f52011-10-25 15:05:16 -0700431
432 int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700433 mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700434
435 // Move back to the beginning of the current word, if any
436 int wordStart = mWordIterator.preceding(start);
437 int wordEnd;
438 if (wordStart == BreakIterator.DONE) {
439 wordEnd = mWordIterator.following(start);
440 if (wordEnd != BreakIterator.DONE) {
441 wordStart = mWordIterator.getBeginning(wordEnd);
442 }
443 } else {
444 wordEnd = mWordIterator.getEnd(wordStart);
445 }
446 if (wordEnd == BreakIterator.DONE) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700447 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700448 return;
449 }
450
451 // We need to expand by one character because we want to include the spans that
452 // end/start at position start/end respectively.
Gilles Debunnef6560302011-10-10 15:03:55 -0700453 SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
454 SpellCheckSpan.class);
455 SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
456 SuggestionSpan.class);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700457
Gilles Debunne35199f52011-10-25 15:05:16 -0700458 int wordCount = 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700459 boolean scheduleOtherSpellCheck = false;
460
461 while (wordStart <= end) {
462 if (wordEnd >= start && wordEnd > wordStart) {
Gilles Debunne35199f52011-10-25 15:05:16 -0700463 if (wordCount >= MAX_NUMBER_OF_WORDS) {
464 scheduleOtherSpellCheck = true;
465 break;
466 }
467
Gilles Debunne287d6c62011-10-05 18:22:11 -0700468 // A new word has been created across the interval boundaries with this edit.
469 // Previous spans (ended on start / started on end) removed, not valid anymore
470 if (wordStart < start && wordEnd > start) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700471 removeSpansAt(editable, start, spellCheckSpans);
472 removeSpansAt(editable, start, suggestionSpans);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700473 }
474
475 if (wordStart < end && wordEnd > end) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700476 removeSpansAt(editable, end, spellCheckSpans);
477 removeSpansAt(editable, end, suggestionSpans);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700478 }
479
480 // Do not create new boundary spans if they already exist
481 boolean createSpellCheckSpan = true;
482 if (wordEnd == start) {
483 for (int i = 0; i < spellCheckSpans.length; i++) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700484 final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700485 if (spanEnd == start) {
486 createSpellCheckSpan = false;
487 break;
488 }
489 }
490 }
491
492 if (wordStart == end) {
493 for (int i = 0; i < spellCheckSpans.length; i++) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700494 final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700495 if (spanStart == end) {
496 createSpellCheckSpan = false;
497 break;
498 }
499 }
500 }
501
502 if (createSpellCheckSpan) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700503 addSpellCheckSpan(editable, wordStart, wordEnd);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700504 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700505 wordCount++;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700506 }
507
508 // iterate word by word
Gilles Debunne35199f52011-10-25 15:05:16 -0700509 int originalWordEnd = wordEnd;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700510 wordEnd = mWordIterator.following(wordEnd);
Gilles Debunne35199f52011-10-25 15:05:16 -0700511 if ((wordIteratorWindowEnd < end) &&
512 (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
513 wordIteratorWindowEnd = Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700514 mWordIterator.setCharSequence(editable, originalWordEnd, wordIteratorWindowEnd);
515 wordEnd = mWordIterator.following(originalWordEnd);
Gilles Debunne35199f52011-10-25 15:05:16 -0700516 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700517 if (wordEnd == BreakIterator.DONE) break;
518 wordStart = mWordIterator.getBeginning(wordEnd);
519 if (wordStart == BreakIterator.DONE) {
520 break;
521 }
522 }
523
524 if (scheduleOtherSpellCheck) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700525 // Update range span: start new spell check from last wordStart
526 setRangeSpan(editable, wordStart, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700527 } else {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700528 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700529 }
530
531 spellCheck();
532 }
533
Gilles Debunnef6560302011-10-10 15:03:55 -0700534 private <T> void removeSpansAt(Editable editable, int offset, T[] spans) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700535 final int length = spans.length;
536 for (int i = 0; i < length; i++) {
537 final T span = spans[i];
Gilles Debunnef6560302011-10-10 15:03:55 -0700538 final int start = editable.getSpanStart(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700539 if (start > offset) continue;
Gilles Debunnef6560302011-10-10 15:03:55 -0700540 final int end = editable.getSpanEnd(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700541 if (end < offset) continue;
Gilles Debunnef6560302011-10-10 15:03:55 -0700542 editable.removeSpan(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700543 }
544 }
545 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700546}