blob: 8adb433a9c0c4c2140190c1620c5acd8330b09ad [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;
satok85894742012-04-20 17:54:51 +090024import android.text.TextUtils;
Gilles Debunne287d6c62011-10-05 18:22:11 -070025import android.text.method.WordIterator;
Gilles Debunne6435a562011-08-04 21:22:30 -070026import android.text.style.SpellCheckSpan;
27import android.text.style.SuggestionSpan;
satoke1e87482012-04-18 16:52:44 +090028import android.util.Log;
satok85894742012-04-20 17:54:51 +090029import android.util.LruCache;
satokd404fe12012-02-22 06:38:18 +090030import android.view.textservice.SentenceSuggestionsInfo;
Gilles Debunne6435a562011-08-04 21:22:30 -070031import android.view.textservice.SpellCheckerSession;
32import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
33import android.view.textservice.SuggestionsInfo;
34import android.view.textservice.TextInfo;
35import android.view.textservice.TextServicesManager;
36
37import com.android.internal.util.ArrayUtils;
38
Gilles Debunne287d6c62011-10-05 18:22:11 -070039import java.text.BreakIterator;
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070040import java.util.Locale;
Gilles Debunne287d6c62011-10-05 18:22:11 -070041
Gilles Debunne6435a562011-08-04 21:22:30 -070042
43/**
44 * Helper class for TextView. Bridge between the TextView and the Dictionnary service.
45 *
46 * @hide
47 */
48public class SpellChecker implements SpellCheckerSessionListener {
satoke1e87482012-04-18 16:52:44 +090049 private static final String TAG = SpellChecker.class.getSimpleName();
50 private static final boolean DBG = false;
Gilles Debunne6435a562011-08-04 21:22:30 -070051
Gilles Debunne35199f52011-10-25 15:05:16 -070052 // No more than this number of words will be parsed on each iteration to ensure a minimum
53 // lock of the UI thread
Gilles Debunnebe5f49f2011-10-25 15:05:16 -070054 public static final int MAX_NUMBER_OF_WORDS = 50;
Gilles Debunne35199f52011-10-25 15:05:16 -070055
Gilles Debunnebe5f49f2011-10-25 15:05:16 -070056 // Rough estimate, such that the word iterator interval usually does not need to be shifted
57 public static final int AVERAGE_WORD_LENGTH = 7;
Gilles Debunne35199f52011-10-25 15:05:16 -070058
59 // When parsing, use a character window of that size. Will be shifted if needed
60 public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS;
61
Gilles Debunnebe5f49f2011-10-25 15:05:16 -070062 // Pause between each spell check to keep the UI smooth
Gilles Debunne35199f52011-10-25 15:05:16 -070063 private final static int SPELL_PAUSE_DURATION = 400; // milliseconds
Gilles Debunne287d6c62011-10-05 18:22:11 -070064
satok979de902012-04-23 13:46:40 +090065 private static final int MIN_SENTENCE_LENGTH = 50;
66
satok88983582011-11-30 15:38:30 +090067 private static final int USE_SPAN_RANGE = -1;
68
Gilles Debunne6435a562011-08-04 21:22:30 -070069 private final TextView mTextView;
70
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070071 SpellCheckerSession mSpellCheckerSession;
satok88983582011-11-30 15:38:30 +090072 // We assume that the sentence level spell check will always provide better results than words.
73 // Although word SC has a sequential option.
74 private boolean mIsSentenceSpellCheckSupported;
Gilles Debunne6435a562011-08-04 21:22:30 -070075 final int mCookie;
76
Gilles Debunneb062e812011-09-27 14:58:37 -070077 // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated
78 // SpellCheckSpan has been recycled and can be-reused.
Gilles Debunne287d6c62011-10-05 18:22:11 -070079 // Contains null SpellCheckSpans after index mLength.
Gilles Debunne6435a562011-08-04 21:22:30 -070080 private int[] mIds;
81 private SpellCheckSpan[] mSpellCheckSpans;
Gilles Debunneb062e812011-09-27 14:58:37 -070082 // The mLength first elements of the above arrays have been initialized
Gilles Debunne6435a562011-08-04 21:22:30 -070083 private int mLength;
84
Gilles Debunne287d6c62011-10-05 18:22:11 -070085 // Parsers on chunck of text, cutting text into words that will be checked
86 private SpellParser[] mSpellParsers = new SpellParser[0];
87
Gilles Debunne6435a562011-08-04 21:22:30 -070088 private int mSpanSequenceCounter = 0;
Gilles Debunne6435a562011-08-04 21:22:30 -070089
Gilles Debunne9d8d3f12011-10-13 12:15:10 -070090 private Locale mCurrentLocale;
91
92 // Shared by all SpellParsers. Cannot be shared with TextView since it may be used
93 // concurrently due to the asynchronous nature of onGetSuggestions.
94 private WordIterator mWordIterator;
95
Gilles Debunnea49ba2f2011-12-01 17:41:15 -080096 private TextServicesManager mTextServicesManager;
97
Gilles Debunnebe5f49f2011-10-25 15:05:16 -070098 private Runnable mSpellRunnable;
99
satok85894742012-04-20 17:54:51 +0900100 private static final int SUGGESTION_SPAN_CACHE_SIZE = 10;
101 private final LruCache<Long, SuggestionSpan> mSuggestionSpanCache =
102 new LruCache<Long, SuggestionSpan>(SUGGESTION_SPAN_CACHE_SIZE);
103
Gilles Debunne6435a562011-08-04 21:22:30 -0700104 public SpellChecker(TextView textView) {
105 mTextView = textView;
106
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700107 // Arbitrary: these arrays will automatically double their sizes on demand
Gilles Debunneb062e812011-09-27 14:58:37 -0700108 final int size = ArrayUtils.idealObjectArraySize(1);
Gilles Debunne6435a562011-08-04 21:22:30 -0700109 mIds = new int[size];
110 mSpellCheckSpans = new SpellCheckSpan[size];
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700111
satok05f24702011-11-02 19:29:35 +0900112 setLocale(mTextView.getTextServicesLocale());
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700113
114 mCookie = hashCode();
115 }
116
Gilles Debunne249d1e82011-12-12 20:06:29 -0800117 private void resetSession() {
Marco Nelissen56735442011-11-09 16:07:33 -0800118 closeSession();
Gilles Debunne249d1e82011-12-12 20:06:29 -0800119
120 mTextServicesManager = (TextServicesManager) mTextView.getContext().
121 getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
satokf43305f2012-01-25 16:05:11 +0900122 if (!mTextServicesManager.isSpellCheckerEnabled()
123 || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) {
satok9b3855b2011-11-02 17:01:28 +0900124 mSpellCheckerSession = null;
125 } else {
Gilles Debunne249d1e82011-12-12 20:06:29 -0800126 mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
satok9b3855b2011-11-02 17:01:28 +0900127 null /* Bundle not currently used by the textServicesManager */,
Gilles Debunne249d1e82011-12-12 20:06:29 -0800128 mCurrentLocale, this,
satok9b3855b2011-11-02 17:01:28 +0900129 false /* means any available languages from current spell checker */);
satokc7ee1b92012-04-11 20:40:07 +0900130 mIsSentenceSpellCheckSupported = true;
satok9b3855b2011-11-02 17:01:28 +0900131 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700132
133 // Restore SpellCheckSpans in pool
134 for (int i = 0; i < mLength; i++) {
satok85894742012-04-20 17:54:51 +0900135 // Resets id and progress to invalidate spell check span
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700136 mSpellCheckSpans[i].setSpellCheckInProgress(false);
137 mIds[i] = -1;
138 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700139 mLength = 0;
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700140
Gilles Debunne249d1e82011-12-12 20:06:29 -0800141 // Remove existing misspelled SuggestionSpans
142 mTextView.removeMisspelledSpans((Editable) mTextView.getText());
satok85894742012-04-20 17:54:51 +0900143 mSuggestionSpanCache.evictAll();
Gilles Debunne249d1e82011-12-12 20:06:29 -0800144 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700145
Gilles Debunne249d1e82011-12-12 20:06:29 -0800146 private void setLocale(Locale locale) {
147 mCurrentLocale = locale;
148
149 resetSession();
150
151 // Change SpellParsers' wordIterator locale
152 mWordIterator = new WordIterator(locale);
153
154 // This class is the listener for locale change: warn other locale-aware objects
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700155 mTextView.onLocaleChanged();
Gilles Debunne6435a562011-08-04 21:22:30 -0700156 }
157
Gilles Debunne186aaf92011-09-16 14:26:12 -0700158 /**
159 * @return true if a spell checker session has successfully been created. Returns false if not,
160 * for instance when spell checking has been disabled in settings.
161 */
Gilles Debunne287d6c62011-10-05 18:22:11 -0700162 private boolean isSessionActive() {
Gilles Debunne186aaf92011-09-16 14:26:12 -0700163 return mSpellCheckerSession != null;
164 }
165
166 public void closeSession() {
167 if (mSpellCheckerSession != null) {
168 mSpellCheckerSession.close();
169 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700170
171 final int length = mSpellParsers.length;
172 for (int i = 0; i < length; i++) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700173 mSpellParsers[i].stop();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700174 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700175
176 if (mSpellRunnable != null) {
177 mTextView.removeCallbacks(mSpellRunnable);
178 }
Gilles Debunne186aaf92011-09-16 14:26:12 -0700179 }
180
Gilles Debunneb062e812011-09-27 14:58:37 -0700181 private int nextSpellCheckSpanIndex() {
182 for (int i = 0; i < mLength; i++) {
183 if (mIds[i] < 0) return i;
184 }
185
186 if (mLength == mSpellCheckSpans.length) {
187 final int newSize = mLength * 2;
Gilles Debunne6435a562011-08-04 21:22:30 -0700188 int[] newIds = new int[newSize];
189 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize];
Gilles Debunneb062e812011-09-27 14:58:37 -0700190 System.arraycopy(mIds, 0, newIds, 0, mLength);
191 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength);
Gilles Debunne6435a562011-08-04 21:22:30 -0700192 mIds = newIds;
193 mSpellCheckSpans = newSpellCheckSpans;
194 }
195
Gilles Debunneb062e812011-09-27 14:58:37 -0700196 mSpellCheckSpans[mLength] = new SpellCheckSpan();
Gilles Debunne6435a562011-08-04 21:22:30 -0700197 mLength++;
Gilles Debunneb062e812011-09-27 14:58:37 -0700198 return mLength - 1;
199 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700200
Gilles Debunnef6560302011-10-10 15:03:55 -0700201 private void addSpellCheckSpan(Editable editable, int start, int end) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700202 final int index = nextSpellCheckSpanIndex();
Gilles Debunnef6560302011-10-10 15:03:55 -0700203 editable.setSpan(mSpellCheckSpans[index], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunneb062e812011-09-27 14:58:37 -0700204 mIds[index] = mSpanSequenceCounter++;
Gilles Debunne6435a562011-08-04 21:22:30 -0700205 }
206
207 public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) {
208 for (int i = 0; i < mLength; i++) {
209 if (mSpellCheckSpans[i] == spellCheckSpan) {
satok85894742012-04-20 17:54:51 +0900210 // Resets id and progress to invalidate spell check span
Gilles Debunneb062e812011-09-27 14:58:37 -0700211 mSpellCheckSpans[i].setSpellCheckInProgress(false);
212 mIds[i] = -1;
Gilles Debunne6435a562011-08-04 21:22:30 -0700213 return;
214 }
215 }
216 }
217
Gilles Debunne6435a562011-08-04 21:22:30 -0700218 public void onSelectionChanged() {
Gilles Debunneb062e812011-09-27 14:58:37 -0700219 spellCheck();
Gilles Debunne6435a562011-08-04 21:22:30 -0700220 }
221
Gilles Debunne287d6c62011-10-05 18:22:11 -0700222 public void spellCheck(int start, int end) {
satok85894742012-04-20 17:54:51 +0900223 if (DBG) {
224 Log.d(TAG, "Start spell-checking: " + start + ", " + end);
225 }
satok05f24702011-11-02 19:29:35 +0900226 final Locale locale = mTextView.getTextServicesLocale();
Gilles Debunnec62589c2012-04-12 14:50:23 -0700227 final boolean isSessionActive = isSessionActive();
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700228 if (mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
229 setLocale(locale);
230 // Re-check the entire text
231 start = 0;
232 end = mTextView.getText().length();
Gilles Debunne249d1e82011-12-12 20:06:29 -0800233 } else {
234 final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
Gilles Debunnec62589c2012-04-12 14:50:23 -0700235 if (isSessionActive != spellCheckerActivated) {
Gilles Debunne249d1e82011-12-12 20:06:29 -0800236 // Spell checker has been turned of or off since last spellCheck
237 resetSession();
238 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700239 }
240
Gilles Debunnec62589c2012-04-12 14:50:23 -0700241 if (!isSessionActive) return;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700242
Gilles Debunnee9b82802011-10-27 14:38:27 -0700243 // Find first available SpellParser from pool
Gilles Debunne287d6c62011-10-05 18:22:11 -0700244 final int length = mSpellParsers.length;
245 for (int i = 0; i < length; i++) {
246 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunne249d1e82011-12-12 20:06:29 -0800247 if (spellParser.isFinished()) {
Gilles Debunne0249b432012-04-09 16:02:31 -0700248 spellParser.parse(start, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700249 return;
250 }
251 }
252
satok85894742012-04-20 17:54:51 +0900253 if (DBG) {
254 Log.d(TAG, "new spell parser.");
255 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700256 // No available parser found in pool, create a new one
257 SpellParser[] newSpellParsers = new SpellParser[length + 1];
258 System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
259 mSpellParsers = newSpellParsers;
260
261 SpellParser spellParser = new SpellParser();
262 mSpellParsers[length] = spellParser;
Gilles Debunne0249b432012-04-09 16:02:31 -0700263 spellParser.parse(start, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700264 }
265
266 private void spellCheck() {
Gilles Debunne0eea6682011-08-29 13:30:31 -0700267 if (mSpellCheckerSession == null) return;
Gilles Debunne99068472011-08-29 12:05:11 -0700268
Gilles Debunnef6560302011-10-10 15:03:55 -0700269 Editable editable = (Editable) mTextView.getText();
270 final int selectionStart = Selection.getSelectionStart(editable);
271 final int selectionEnd = Selection.getSelectionEnd(editable);
Gilles Debunne6435a562011-08-04 21:22:30 -0700272
273 TextInfo[] textInfos = new TextInfo[mLength];
274 int textInfosCount = 0;
275
276 for (int i = 0; i < mLength; i++) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700277 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
satok85894742012-04-20 17:54:51 +0900278 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue;
Gilles Debunne6435a562011-08-04 21:22:30 -0700279
Gilles Debunnef6560302011-10-10 15:03:55 -0700280 final int start = editable.getSpanStart(spellCheckSpan);
281 final int end = editable.getSpanEnd(spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700282
283 // Do not check this word if the user is currently editing it
satok85894742012-04-20 17:54:51 +0900284 final boolean isEditing;
285 if (mIsSentenceSpellCheckSupported) {
286 // Allow the overlap of the cursor and the first boundary of the spell check span
287 // no to skip the spell check of the following word because the
288 // following word will never be spell-checked even if the user finishes composing
289 isEditing = selectionEnd <= start || selectionStart > end;
290 } else {
291 isEditing = selectionEnd < start || selectionStart > end;
292 }
293 if (start >= 0 && end > start && isEditing) {
Gilles Debunne653d3a22011-12-07 10:35:59 -0800294 final String word = (editable instanceof SpannableStringBuilder) ?
295 ((SpannableStringBuilder) editable).substring(start, end) :
296 editable.subSequence(start, end).toString();
Gilles Debunneb062e812011-09-27 14:58:37 -0700297 spellCheckSpan.setSpellCheckInProgress(true);
Gilles Debunne6435a562011-08-04 21:22:30 -0700298 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
satoke1e87482012-04-18 16:52:44 +0900299 if (DBG) {
300 Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ")" + word
301 + ", cookie = " + mCookie + ", seq = "
302 + mIds[i] + ", sel start = " + selectionStart + ", sel end = "
303 + selectionEnd + ", start = " + start + ", end = " + end);
304 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700305 }
306 }
307
308 if (textInfosCount > 0) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700309 if (textInfosCount < textInfos.length) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700310 TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
311 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
312 textInfos = textInfosCopy;
313 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700314
satok88983582011-11-30 15:38:30 +0900315 if (mIsSentenceSpellCheckSupported) {
316 mSpellCheckerSession.getSentenceSuggestions(
317 textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE);
318 } else {
319 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
320 false /* TODO Set sequentialWords to true for initial spell check */);
321 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700322 }
323 }
324
satok88983582011-11-30 15:38:30 +0900325 private SpellCheckSpan onGetSuggestionsInternal(
326 SuggestionsInfo suggestionsInfo, int offset, int length) {
satok792ee0c2012-03-08 17:03:48 +0900327 if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) {
satok88983582011-11-30 15:38:30 +0900328 return null;
329 }
330 final Editable editable = (Editable) mTextView.getText();
331 final int sequenceNumber = suggestionsInfo.getSequence();
332 for (int k = 0; k < mLength; ++k) {
333 if (sequenceNumber == mIds[k]) {
334 final int attributes = suggestionsInfo.getSuggestionsAttributes();
335 final boolean isInDictionary =
336 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
337 final boolean looksLikeTypo =
338 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
339
340 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k];
341 //TODO: we need to change that rule for results from a sentence-level spell
342 // checker that will probably be in dictionary.
343 if (!isInDictionary && looksLikeTypo) {
344 createMisspelledSuggestionSpan(
345 editable, suggestionsInfo, spellCheckSpan, offset, length);
346 }
347 return spellCheckSpan;
348 }
349 }
350 return null;
satok0dc1f642011-11-18 11:27:10 +0900351 }
352
353 @Override
Gilles Debunne6435a562011-08-04 21:22:30 -0700354 public void onGetSuggestions(SuggestionsInfo[] results) {
satok88983582011-11-30 15:38:30 +0900355 final Editable editable = (Editable) mTextView.getText();
356 for (int i = 0; i < results.length; ++i) {
357 final SpellCheckSpan spellCheckSpan =
358 onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE);
359 if (spellCheckSpan != null) {
360 editable.removeSpan(spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700361 }
362 }
satok88983582011-11-30 15:38:30 +0900363 scheduleNewSpellCheck();
364 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700365
satok88983582011-11-30 15:38:30 +0900366 @Override
367 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
368 final Editable editable = (Editable) mTextView.getText();
369
370 for (int i = 0; i < results.length; ++i) {
371 final SentenceSuggestionsInfo ssi = results[i];
satok792ee0c2012-03-08 17:03:48 +0900372 if (ssi == null) {
373 continue;
374 }
satok88983582011-11-30 15:38:30 +0900375 SpellCheckSpan spellCheckSpan = null;
376 for (int j = 0; j < ssi.getSuggestionsCount(); ++j) {
377 final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j);
satok792ee0c2012-03-08 17:03:48 +0900378 if (suggestionsInfo == null) {
379 continue;
380 }
satok88983582011-11-30 15:38:30 +0900381 final int offset = ssi.getOffsetAt(j);
382 final int length = ssi.getLengthAt(j);
383 final SpellCheckSpan scs = onGetSuggestionsInternal(
384 suggestionsInfo, offset, length);
385 if (spellCheckSpan == null && scs != null) {
386 // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same
387 // SentenceSuggestionsInfo
388 spellCheckSpan = scs;
389 }
390 }
391 if (spellCheckSpan != null) {
392 editable.removeSpan(spellCheckSpan);
393 }
394 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700395 scheduleNewSpellCheck();
396 }
397
398 private void scheduleNewSpellCheck() {
satok85894742012-04-20 17:54:51 +0900399 if (DBG) {
400 Log.i(TAG, "schedule new spell check.");
401 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700402 if (mSpellRunnable == null) {
403 mSpellRunnable = new Runnable() {
404 @Override
405 public void run() {
406 final int length = mSpellParsers.length;
407 for (int i = 0; i < length; i++) {
408 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunne249d1e82011-12-12 20:06:29 -0800409 if (!spellParser.isFinished()) {
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700410 spellParser.parse();
411 break; // run one spell parser at a time to bound running time
412 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700413 }
414 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700415 };
416 } else {
417 mTextView.removeCallbacks(mSpellRunnable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700418 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700419
420 mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
Gilles Debunne6435a562011-08-04 21:22:30 -0700421 }
422
Gilles Debunne8615ac92011-11-29 15:25:03 -0800423 private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
satok88983582011-11-30 15:38:30 +0900424 SpellCheckSpan spellCheckSpan, int offset, int length) {
425 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
426 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
427 if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart)
428 return; // span was removed in the meantime
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700429
satok88983582011-11-30 15:38:30 +0900430 final int start;
431 final int end;
432 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
433 start = spellCheckSpanStart + offset;
434 end = start + length;
435 } else {
436 start = spellCheckSpanStart;
437 end = spellCheckSpanEnd;
438 }
439
Gilles Debunne41347e92012-05-08 15:39:15 -0700440 final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
441 String[] suggestions;
442 if (suggestionsCount > 0) {
443 suggestions = new String[suggestionsCount];
444 for (int i = 0; i < suggestionsCount; i++) {
445 suggestions[i] = suggestionsInfo.getSuggestionAt(i);
446 }
447 } else {
448 suggestions = ArrayUtils.emptyArray(String.class);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700449 }
450
451 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
452 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
satok85894742012-04-20 17:54:51 +0900453 // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
454 // to share the logic of word level spell checker and sentence level spell checker
455 if (mIsSentenceSpellCheckSupported) {
Gilles Debunne41347e92012-05-08 15:39:15 -0700456 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
satok85894742012-04-20 17:54:51 +0900457 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
458 if (tempSuggestionSpan != null) {
459 if (DBG) {
460 Log.i(TAG, "Cached span on the same position is cleard. "
461 + editable.subSequence(start, end));
462 }
463 editable.removeSpan(tempSuggestionSpan);
464 }
465 mSuggestionSpanCache.put(key, suggestionSpan);
466 }
Gilles Debunnef6560302011-10-10 15:03:55 -0700467 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700468
Gilles Debunne961ebb92011-12-12 10:16:04 -0800469 mTextView.invalidateRegion(start, end, false /* No cursor involved */);
Gilles Debunne6435a562011-08-04 21:22:30 -0700470 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700471
472 private class SpellParser {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700473 private Object mRange = new Object();
474
Gilles Debunne0249b432012-04-09 16:02:31 -0700475 public void parse(int start, int end) {
476 if (end > start) {
477 setRangeSpan((Editable) mTextView.getText(), start, end);
478 parse();
479 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700480 }
481
Gilles Debunne249d1e82011-12-12 20:06:29 -0800482 public boolean isFinished() {
483 return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700484 }
485
Gilles Debunne249d1e82011-12-12 20:06:29 -0800486 public void stop() {
487 removeRangeSpan((Editable) mTextView.getText());
Gilles Debunnee9b82802011-10-27 14:38:27 -0700488 }
489
490 private void setRangeSpan(Editable editable, int start, int end) {
satok85894742012-04-20 17:54:51 +0900491 if (DBG) {
492 Log.d(TAG, "set next range span: " + start + ", " + end);
493 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700494 editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
495 }
496
497 private void removeRangeSpan(Editable editable) {
satok85894742012-04-20 17:54:51 +0900498 if (DBG) {
499 Log.d(TAG, "Remove range span." + editable.getSpanStart(editable)
500 + editable.getSpanEnd(editable));
501 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700502 editable.removeSpan(mRange);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700503 }
504
505 public void parse() {
Gilles Debunnef6560302011-10-10 15:03:55 -0700506 Editable editable = (Editable) mTextView.getText();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700507 // Iterate over the newly added text and schedule new SpellCheckSpans
satok24d146b2012-04-26 20:44:34 +0900508 final int start;
509 if (mIsSentenceSpellCheckSupported) {
510 // TODO: Find the start position of the sentence.
511 // Set span with the context
512 start = Math.max(
513 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH);
514 } else {
515 start = editable.getSpanStart(mRange);
516 }
517
Gilles Debunnef6560302011-10-10 15:03:55 -0700518 final int end = editable.getSpanEnd(mRange);
Gilles Debunne35199f52011-10-25 15:05:16 -0700519
520 int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700521 mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700522
523 // Move back to the beginning of the current word, if any
524 int wordStart = mWordIterator.preceding(start);
525 int wordEnd;
526 if (wordStart == BreakIterator.DONE) {
527 wordEnd = mWordIterator.following(start);
528 if (wordEnd != BreakIterator.DONE) {
529 wordStart = mWordIterator.getBeginning(wordEnd);
530 }
531 } else {
532 wordEnd = mWordIterator.getEnd(wordStart);
533 }
534 if (wordEnd == BreakIterator.DONE) {
satok85894742012-04-20 17:54:51 +0900535 if (DBG) {
536 Log.i(TAG, "No more spell check.");
537 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700538 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700539 return;
540 }
541
542 // We need to expand by one character because we want to include the spans that
543 // end/start at position start/end respectively.
Gilles Debunnef6560302011-10-10 15:03:55 -0700544 SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
545 SpellCheckSpan.class);
546 SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
547 SuggestionSpan.class);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700548
Gilles Debunne35199f52011-10-25 15:05:16 -0700549 int wordCount = 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700550 boolean scheduleOtherSpellCheck = false;
551
satok88983582011-11-30 15:38:30 +0900552 if (mIsSentenceSpellCheckSupported) {
satok88983582011-11-30 15:38:30 +0900553 if (wordIteratorWindowEnd < end) {
satok85894742012-04-20 17:54:51 +0900554 if (DBG) {
555 Log.i(TAG, "schedule other spell check.");
556 }
satok88983582011-11-30 15:38:30 +0900557 // Several batches needed on that region. Cut after last previous word
satok88983582011-11-30 15:38:30 +0900558 scheduleOtherSpellCheck = true;
satok88983582011-11-30 15:38:30 +0900559 }
satok85894742012-04-20 17:54:51 +0900560 int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd);
561 boolean correct = spellCheckEnd != BreakIterator.DONE;
satok88983582011-11-30 15:38:30 +0900562 if (correct) {
satok85894742012-04-20 17:54:51 +0900563 spellCheckEnd = mWordIterator.getEnd(spellCheckEnd);
564 correct = spellCheckEnd != BreakIterator.DONE;
satok88983582011-11-30 15:38:30 +0900565 }
566 if (!correct) {
satok85894742012-04-20 17:54:51 +0900567 if (DBG) {
568 Log.i(TAG, "Incorrect range span.");
569 }
570 removeRangeSpan(editable);
satok88983582011-11-30 15:38:30 +0900571 return;
572 }
satok85894742012-04-20 17:54:51 +0900573 do {
574 // TODO: Find the start position of the sentence.
575 int spellCheckStart = wordStart;
576 boolean createSpellCheckSpan = true;
577 // Cancel or merge overlapped spell check spans
578 for (int i = 0; i < mLength; ++i) {
579 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
580 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) {
581 continue;
582 }
583 final int spanStart = editable.getSpanStart(spellCheckSpan);
584 final int spanEnd = editable.getSpanEnd(spellCheckSpan);
585 if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) {
586 // No need to merge
587 continue;
588 }
589 if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) {
590 // There is a completely overlapped spell check span
591 // skip this span
592 createSpellCheckSpan = false;
593 if (DBG) {
594 Log.i(TAG, "The range is overrapped. Skip spell check.");
595 }
596 break;
597 }
598 removeSpellCheckSpan(spellCheckSpan);
599 spellCheckStart = Math.min(spanStart, spellCheckStart);
600 spellCheckEnd = Math.max(spanEnd, spellCheckEnd);
601 }
602
603 if (DBG) {
604 Log.d(TAG, "addSpellCheckSpan: "
605 + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart
606 + ", next = " + scheduleOtherSpellCheck + "\n"
607 + editable.subSequence(spellCheckStart, spellCheckEnd));
608 }
609
610 // Stop spell checking when there are no characters in the range.
611 if (spellCheckEnd < start) {
612 break;
613 }
614 if (createSpellCheckSpan) {
615 addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd);
616 }
617 } while (false);
618 wordStart = spellCheckEnd;
satok88983582011-11-30 15:38:30 +0900619 } else {
620 while (wordStart <= end) {
621 if (wordEnd >= start && wordEnd > wordStart) {
622 if (wordCount >= MAX_NUMBER_OF_WORDS) {
623 scheduleOtherSpellCheck = true;
624 break;
625 }
626 // A new word has been created across the interval boundaries with this
627 // edit. The previous spans (that ended on start / started on end) are
628 // not valid anymore and must be removed.
629 if (wordStart < start && wordEnd > start) {
630 removeSpansAt(editable, start, spellCheckSpans);
631 removeSpansAt(editable, start, suggestionSpans);
632 }
633
634 if (wordStart < end && wordEnd > end) {
635 removeSpansAt(editable, end, spellCheckSpans);
636 removeSpansAt(editable, end, suggestionSpans);
637 }
638
639 // Do not create new boundary spans if they already exist
640 boolean createSpellCheckSpan = true;
641 if (wordEnd == start) {
642 for (int i = 0; i < spellCheckSpans.length; i++) {
643 final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
644 if (spanEnd == start) {
645 createSpellCheckSpan = false;
646 break;
647 }
648 }
649 }
650
651 if (wordStart == end) {
652 for (int i = 0; i < spellCheckSpans.length; i++) {
653 final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
654 if (spanStart == end) {
655 createSpellCheckSpan = false;
656 break;
657 }
658 }
659 }
660
661 if (createSpellCheckSpan) {
662 addSpellCheckSpan(editable, wordStart, wordEnd);
663 }
664 wordCount++;
665 }
666
667 // iterate word by word
668 int originalWordEnd = wordEnd;
669 wordEnd = mWordIterator.following(wordEnd);
670 if ((wordIteratorWindowEnd < end) &&
671 (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
672 wordIteratorWindowEnd =
673 Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
674 mWordIterator.setCharSequence(
675 editable, originalWordEnd, wordIteratorWindowEnd);
676 wordEnd = mWordIterator.following(originalWordEnd);
677 }
678 if (wordEnd == BreakIterator.DONE) break;
679 wordStart = mWordIterator.getBeginning(wordEnd);
680 if (wordStart == BreakIterator.DONE) {
Gilles Debunne35199f52011-10-25 15:05:16 -0700681 break;
682 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700683 }
684 }
685
686 if (scheduleOtherSpellCheck) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700687 // Update range span: start new spell check from last wordStart
688 setRangeSpan(editable, wordStart, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700689 } else {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700690 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700691 }
692
693 spellCheck();
694 }
695
Gilles Debunnef6560302011-10-10 15:03:55 -0700696 private <T> void removeSpansAt(Editable editable, int offset, T[] spans) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700697 final int length = spans.length;
698 for (int i = 0; i < length; i++) {
699 final T span = spans[i];
Gilles Debunnef6560302011-10-10 15:03:55 -0700700 final int start = editable.getSpanStart(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700701 if (start > offset) continue;
Gilles Debunnef6560302011-10-10 15:03:55 -0700702 final int end = editable.getSpanEnd(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700703 if (end < offset) continue;
Gilles Debunnef6560302011-10-10 15:03:55 -0700704 editable.removeSpan(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700705 }
706 }
707 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700708}