blob: 67008292e590192f0ba0fd6230fd5c2b25d45361 [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
100 private void setLocale(Locale locale) {
Marco Nelissen56735442011-11-09 16:07:33 -0800101 closeSession();
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700102 final TextServicesManager textServicesManager = (TextServicesManager)
103 mTextView.getContext().getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
satok9b3855b2011-11-02 17:01:28 +0900104 if (!textServicesManager.isSpellCheckerEnabled()) {
105 mSpellCheckerSession = null;
106 } else {
107 mSpellCheckerSession = textServicesManager.newSpellCheckerSession(
108 null /* Bundle not currently used by the textServicesManager */,
109 locale, this,
110 false /* means any available languages from current spell checker */);
111 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700112 mCurrentLocale = locale;
113
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 Debunnee9b82802011-10-27 14:38:27 -0700121 mSpellParsers = new SpellParser[0];
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700122
Gilles Debunnee9b82802011-10-27 14:38:27 -0700123 // This class is the global listener for locale change: warn other locale-aware objects
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700124 mTextView.onLocaleChanged();
Gilles Debunne6435a562011-08-04 21:22:30 -0700125 }
126
Gilles Debunne186aaf92011-09-16 14:26:12 -0700127 /**
128 * @return true if a spell checker session has successfully been created. Returns false if not,
129 * for instance when spell checking has been disabled in settings.
130 */
Gilles Debunne287d6c62011-10-05 18:22:11 -0700131 private boolean isSessionActive() {
Gilles Debunne186aaf92011-09-16 14:26:12 -0700132 return mSpellCheckerSession != null;
133 }
134
135 public void closeSession() {
136 if (mSpellCheckerSession != null) {
137 mSpellCheckerSession.close();
138 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700139
Gilles Debunnee9b82802011-10-27 14:38:27 -0700140 stopAllSpellParsers();
141 }
142
143 private void stopAllSpellParsers() {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700144 final int length = mSpellParsers.length;
145 for (int i = 0; i < length; i++) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700146 mSpellParsers[i].stop();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700147 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700148
149 if (mSpellRunnable != null) {
150 mTextView.removeCallbacks(mSpellRunnable);
151 }
Gilles Debunne186aaf92011-09-16 14:26:12 -0700152 }
153
Gilles Debunneb062e812011-09-27 14:58:37 -0700154 private int nextSpellCheckSpanIndex() {
155 for (int i = 0; i < mLength; i++) {
156 if (mIds[i] < 0) return i;
157 }
158
159 if (mLength == mSpellCheckSpans.length) {
160 final int newSize = mLength * 2;
Gilles Debunne6435a562011-08-04 21:22:30 -0700161 int[] newIds = new int[newSize];
162 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize];
Gilles Debunneb062e812011-09-27 14:58:37 -0700163 System.arraycopy(mIds, 0, newIds, 0, mLength);
164 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength);
Gilles Debunne6435a562011-08-04 21:22:30 -0700165 mIds = newIds;
166 mSpellCheckSpans = newSpellCheckSpans;
167 }
168
Gilles Debunneb062e812011-09-27 14:58:37 -0700169 mSpellCheckSpans[mLength] = new SpellCheckSpan();
Gilles Debunne6435a562011-08-04 21:22:30 -0700170 mLength++;
Gilles Debunneb062e812011-09-27 14:58:37 -0700171 return mLength - 1;
172 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700173
Gilles Debunnef6560302011-10-10 15:03:55 -0700174 private void addSpellCheckSpan(Editable editable, int start, int end) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700175 final int index = nextSpellCheckSpanIndex();
Gilles Debunnef6560302011-10-10 15:03:55 -0700176 editable.setSpan(mSpellCheckSpans[index], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunneb062e812011-09-27 14:58:37 -0700177 mIds[index] = mSpanSequenceCounter++;
Gilles Debunne6435a562011-08-04 21:22:30 -0700178 }
179
180 public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) {
181 for (int i = 0; i < mLength; i++) {
182 if (mSpellCheckSpans[i] == spellCheckSpan) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700183 mSpellCheckSpans[i].setSpellCheckInProgress(false);
184 mIds[i] = -1;
Gilles Debunne6435a562011-08-04 21:22:30 -0700185 return;
186 }
187 }
188 }
189
Gilles Debunne6435a562011-08-04 21:22:30 -0700190 public void onSelectionChanged() {
Gilles Debunneb062e812011-09-27 14:58:37 -0700191 spellCheck();
Gilles Debunne6435a562011-08-04 21:22:30 -0700192 }
193
Gilles Debunne287d6c62011-10-05 18:22:11 -0700194 public void spellCheck(int start, int end) {
satok05f24702011-11-02 19:29:35 +0900195 final Locale locale = mTextView.getTextServicesLocale();
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700196 if (mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
197 setLocale(locale);
198 // Re-check the entire text
199 start = 0;
200 end = mTextView.getText().length();
201 }
202
Gilles Debunne287d6c62011-10-05 18:22:11 -0700203 if (!isSessionActive()) return;
204
Gilles Debunnee9b82802011-10-27 14:38:27 -0700205 // Find first available SpellParser from pool
Gilles Debunne287d6c62011-10-05 18:22:11 -0700206 final int length = mSpellParsers.length;
207 for (int i = 0; i < length; i++) {
208 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunnee9b82802011-10-27 14:38:27 -0700209 if (!spellParser.isParsing()) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700210 spellParser.init(start, end);
211 spellParser.parse();
212 return;
213 }
214 }
215
216 // No available parser found in pool, create a new one
217 SpellParser[] newSpellParsers = new SpellParser[length + 1];
218 System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
219 mSpellParsers = newSpellParsers;
220
221 SpellParser spellParser = new SpellParser();
222 mSpellParsers[length] = spellParser;
223 spellParser.init(start, end);
224 spellParser.parse();
225 }
226
227 private void spellCheck() {
Gilles Debunne0eea6682011-08-29 13:30:31 -0700228 if (mSpellCheckerSession == null) return;
Gilles Debunne99068472011-08-29 12:05:11 -0700229
Gilles Debunnef6560302011-10-10 15:03:55 -0700230 Editable editable = (Editable) mTextView.getText();
231 final int selectionStart = Selection.getSelectionStart(editable);
232 final int selectionEnd = Selection.getSelectionEnd(editable);
Gilles Debunne6435a562011-08-04 21:22:30 -0700233
234 TextInfo[] textInfos = new TextInfo[mLength];
235 int textInfosCount = 0;
236
237 for (int i = 0; i < mLength; i++) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700238 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
Gilles Debunne6435a562011-08-04 21:22:30 -0700239 if (spellCheckSpan.isSpellCheckInProgress()) continue;
240
Gilles Debunnef6560302011-10-10 15:03:55 -0700241 final int start = editable.getSpanStart(spellCheckSpan);
242 final int end = editable.getSpanEnd(spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700243
244 // Do not check this word if the user is currently editing it
Gilles Debunned6e34942011-08-26 10:13:15 -0700245 if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) {
Gilles Debunne653d3a22011-12-07 10:35:59 -0800246 final String word = (editable instanceof SpannableStringBuilder) ?
247 ((SpannableStringBuilder) editable).substring(start, end) :
248 editable.subSequence(start, end).toString();
Gilles Debunneb062e812011-09-27 14:58:37 -0700249 spellCheckSpan.setSpellCheckInProgress(true);
Gilles Debunne6435a562011-08-04 21:22:30 -0700250 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
251 }
252 }
253
254 if (textInfosCount > 0) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700255 if (textInfosCount < textInfos.length) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700256 TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
257 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
258 textInfos = textInfosCopy;
259 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700260
Gilles Debunne0eea6682011-08-29 13:30:31 -0700261 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
Gilles Debunne6435a562011-08-04 21:22:30 -0700262 false /* TODO Set sequentialWords to true for initial spell check */);
263 }
264 }
265
266 @Override
satok0dc1f642011-11-18 11:27:10 +0900267 public void onGetSuggestionsForSentence(SuggestionsInfo[] results) {
268 // TODO: Handle the position and length for each suggestion
269 onGetSuggestions(results);
270 }
271
272 @Override
Gilles Debunne6435a562011-08-04 21:22:30 -0700273 public void onGetSuggestions(SuggestionsInfo[] results) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700274 Editable editable = (Editable) mTextView.getText();
275
Gilles Debunne6435a562011-08-04 21:22:30 -0700276 for (int i = 0; i < results.length; i++) {
277 SuggestionsInfo suggestionsInfo = results[i];
278 if (suggestionsInfo.getCookie() != mCookie) continue;
Gilles Debunne6435a562011-08-04 21:22:30 -0700279 final int sequenceNumber = suggestionsInfo.getSequence();
Gilles Debunneb062e812011-09-27 14:58:37 -0700280
281 for (int j = 0; j < mLength; j++) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700282 if (sequenceNumber == mIds[j]) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700283 final int attributes = suggestionsInfo.getSuggestionsAttributes();
284 boolean isInDictionary =
285 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
286 boolean looksLikeTypo =
287 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
288
Gilles Debunnee1fc4f62011-10-03 17:01:19 -0700289 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
Gilles Debunne8615ac92011-11-29 15:25:03 -0800290
Gilles Debunne6435a562011-08-04 21:22:30 -0700291 if (!isInDictionary && looksLikeTypo) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700292 createMisspelledSuggestionSpan(editable, suggestionsInfo, spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700293 }
Gilles Debunne8615ac92011-11-29 15:25:03 -0800294
Gilles Debunnef6560302011-10-10 15:03:55 -0700295 editable.removeSpan(spellCheckSpan);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700296 break;
Gilles Debunne6435a562011-08-04 21:22:30 -0700297 }
298 }
299 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700300
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700301 scheduleNewSpellCheck();
302 }
303
304 private void scheduleNewSpellCheck() {
305 if (mSpellRunnable == null) {
306 mSpellRunnable = new Runnable() {
307 @Override
308 public void run() {
309 final int length = mSpellParsers.length;
310 for (int i = 0; i < length; i++) {
311 final SpellParser spellParser = mSpellParsers[i];
312 if (!spellParser.isFinished()) {
313 spellParser.parse();
314 break; // run one spell parser at a time to bound running time
315 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700316 }
317 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700318 };
319 } else {
320 mTextView.removeCallbacks(mSpellRunnable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700321 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700322
323 mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
Gilles Debunne6435a562011-08-04 21:22:30 -0700324 }
325
Gilles Debunne8615ac92011-11-29 15:25:03 -0800326 private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
327 SpellCheckSpan spellCheckSpan) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700328 final int start = editable.getSpanStart(spellCheckSpan);
329 final int end = editable.getSpanEnd(spellCheckSpan);
Gilles Debunne8615ac92011-11-29 15:25:03 -0800330 if (start < 0 || end <= start) return; // span was removed in the meantime
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700331
332 // Other suggestion spans may exist on that region, with identical suggestions, filter
Gilles Debunne8615ac92011-11-29 15:25:03 -0800333 // them out to avoid duplicates.
Gilles Debunnef6560302011-10-10 15:03:55 -0700334 SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700335 final int length = suggestionSpans.length;
336 for (int i = 0; i < length; i++) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700337 final int spanStart = editable.getSpanStart(suggestionSpans[i]);
338 final int spanEnd = editable.getSpanEnd(suggestionSpans[i]);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700339 if (spanStart != start || spanEnd != end) {
Gilles Debunne8615ac92011-11-29 15:25:03 -0800340 // Nulled (to avoid new array allocation) if not on that exact same region
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700341 suggestionSpans[i] = null;
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700342 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700343 }
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700344
345 final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
346 String[] suggestions;
347 if (suggestionsCount <= 0) {
348 // A negative suggestion count is possible
349 suggestions = ArrayUtils.emptyArray(String.class);
350 } else {
351 int numberOfSuggestions = 0;
352 suggestions = new String[suggestionsCount];
353
354 for (int i = 0; i < suggestionsCount; i++) {
355 final String spellSuggestion = suggestionsInfo.getSuggestionAt(i);
356 if (spellSuggestion == null) break;
357 boolean suggestionFound = false;
358
359 for (int j = 0; j < length && !suggestionFound; j++) {
360 if (suggestionSpans[j] == null) break;
361
362 String[] suggests = suggestionSpans[j].getSuggestions();
363 for (int k = 0; k < suggests.length; k++) {
364 if (spellSuggestion.equals(suggests[k])) {
365 // The suggestion is already provided by an other SuggestionSpan
366 suggestionFound = true;
367 break;
368 }
369 }
370 }
371
372 if (!suggestionFound) {
373 suggestions[numberOfSuggestions++] = spellSuggestion;
374 }
375 }
376
377 if (numberOfSuggestions != suggestionsCount) {
378 String[] newSuggestions = new String[numberOfSuggestions];
379 System.arraycopy(suggestions, 0, newSuggestions, 0, numberOfSuggestions);
380 suggestions = newSuggestions;
381 }
382 }
383
384 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
385 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
Gilles Debunnef6560302011-10-10 15:03:55 -0700386 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700387
Gilles Debunne8615ac92011-11-29 15:25:03 -0800388 mTextView.invalidateRegion(start, end);
Gilles Debunne6435a562011-08-04 21:22:30 -0700389 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700390
391 private class SpellParser {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700392 private Object mRange = new Object();
393
394 public void init(int start, int end) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700395 setRangeSpan((Editable) mTextView.getText(), start, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700396 }
397
Gilles Debunnee9b82802011-10-27 14:38:27 -0700398 public void stop() {
399 removeRangeSpan((Editable) mTextView.getText());
Gilles Debunne287d6c62011-10-05 18:22:11 -0700400 }
401
Gilles Debunnee9b82802011-10-27 14:38:27 -0700402 public boolean isParsing() {
403 return ((Editable) mTextView.getText()).getSpanStart(mRange) >= 0;
404 }
405
406 private void setRangeSpan(Editable editable, int start, int end) {
407 editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
408 }
409
410 private void removeRangeSpan(Editable editable) {
411 editable.removeSpan(mRange);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700412 }
413
414 public void parse() {
Gilles Debunnef6560302011-10-10 15:03:55 -0700415 Editable editable = (Editable) mTextView.getText();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700416 // Iterate over the newly added text and schedule new SpellCheckSpans
Gilles Debunnef6560302011-10-10 15:03:55 -0700417 final int start = editable.getSpanStart(mRange);
418 final int end = editable.getSpanEnd(mRange);
Gilles Debunne35199f52011-10-25 15:05:16 -0700419
420 int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700421 mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700422
423 // Move back to the beginning of the current word, if any
424 int wordStart = mWordIterator.preceding(start);
425 int wordEnd;
426 if (wordStart == BreakIterator.DONE) {
427 wordEnd = mWordIterator.following(start);
428 if (wordEnd != BreakIterator.DONE) {
429 wordStart = mWordIterator.getBeginning(wordEnd);
430 }
431 } else {
432 wordEnd = mWordIterator.getEnd(wordStart);
433 }
434 if (wordEnd == BreakIterator.DONE) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700435 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700436 return;
437 }
438
439 // We need to expand by one character because we want to include the spans that
440 // end/start at position start/end respectively.
Gilles Debunnef6560302011-10-10 15:03:55 -0700441 SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
442 SpellCheckSpan.class);
443 SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
444 SuggestionSpan.class);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700445
Gilles Debunne35199f52011-10-25 15:05:16 -0700446 int wordCount = 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700447 boolean scheduleOtherSpellCheck = false;
448
449 while (wordStart <= end) {
450 if (wordEnd >= start && wordEnd > wordStart) {
Gilles Debunne35199f52011-10-25 15:05:16 -0700451 if (wordCount >= MAX_NUMBER_OF_WORDS) {
452 scheduleOtherSpellCheck = true;
453 break;
454 }
455
Gilles Debunne287d6c62011-10-05 18:22:11 -0700456 // A new word has been created across the interval boundaries with this edit.
457 // Previous spans (ended on start / started on end) removed, not valid anymore
458 if (wordStart < start && wordEnd > start) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700459 removeSpansAt(editable, start, spellCheckSpans);
460 removeSpansAt(editable, start, suggestionSpans);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700461 }
462
463 if (wordStart < end && wordEnd > end) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700464 removeSpansAt(editable, end, spellCheckSpans);
465 removeSpansAt(editable, end, suggestionSpans);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700466 }
467
468 // Do not create new boundary spans if they already exist
469 boolean createSpellCheckSpan = true;
470 if (wordEnd == start) {
471 for (int i = 0; i < spellCheckSpans.length; i++) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700472 final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700473 if (spanEnd == start) {
474 createSpellCheckSpan = false;
475 break;
476 }
477 }
478 }
479
480 if (wordStart == end) {
481 for (int i = 0; i < spellCheckSpans.length; i++) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700482 final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700483 if (spanStart == end) {
484 createSpellCheckSpan = false;
485 break;
486 }
487 }
488 }
489
490 if (createSpellCheckSpan) {
Gilles Debunnef6560302011-10-10 15:03:55 -0700491 addSpellCheckSpan(editable, wordStart, wordEnd);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700492 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700493 wordCount++;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700494 }
495
496 // iterate word by word
Gilles Debunne35199f52011-10-25 15:05:16 -0700497 int originalWordEnd = wordEnd;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700498 wordEnd = mWordIterator.following(wordEnd);
Gilles Debunne35199f52011-10-25 15:05:16 -0700499 if ((wordIteratorWindowEnd < end) &&
500 (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
501 wordIteratorWindowEnd = Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700502 mWordIterator.setCharSequence(editable, originalWordEnd, wordIteratorWindowEnd);
503 wordEnd = mWordIterator.following(originalWordEnd);
Gilles Debunne35199f52011-10-25 15:05:16 -0700504 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700505 if (wordEnd == BreakIterator.DONE) break;
506 wordStart = mWordIterator.getBeginning(wordEnd);
507 if (wordStart == BreakIterator.DONE) {
508 break;
509 }
510 }
511
512 if (scheduleOtherSpellCheck) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700513 // Update range span: start new spell check from last wordStart
514 setRangeSpan(editable, wordStart, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700515 } else {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700516 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700517 }
518
519 spellCheck();
520 }
521
Gilles Debunnef6560302011-10-10 15:03:55 -0700522 private <T> void removeSpansAt(Editable editable, int offset, T[] spans) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700523 final int length = spans.length;
524 for (int i = 0; i < length; i++) {
525 final T span = spans[i];
Gilles Debunnef6560302011-10-10 15:03:55 -0700526 final int start = editable.getSpanStart(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700527 if (start > offset) continue;
Gilles Debunnef6560302011-10-10 15:03:55 -0700528 final int end = editable.getSpanEnd(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700529 if (end < offset) continue;
Gilles Debunnef6560302011-10-10 15:03:55 -0700530 editable.removeSpan(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700531 }
532 }
533 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700534}