blob: 9e7f97ea16ebdd9c6582f45b16c95d9134111471 [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
Satoshi Kataoka5bb4ee6d2012-12-05 22:25:48 +0900112 setLocale(mTextView.getSpellCheckerLocale());
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()
Satoshi Kataoka5bb4ee6d2012-12-05 22:25:48 +0900123 || mCurrentLocale == null
124 || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) {
satok9b3855b2011-11-02 17:01:28 +0900125 mSpellCheckerSession = null;
126 } else {
Gilles Debunne249d1e82011-12-12 20:06:29 -0800127 mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
satok9b3855b2011-11-02 17:01:28 +0900128 null /* Bundle not currently used by the textServicesManager */,
Gilles Debunne249d1e82011-12-12 20:06:29 -0800129 mCurrentLocale, this,
satok9b3855b2011-11-02 17:01:28 +0900130 false /* means any available languages from current spell checker */);
satokc7ee1b92012-04-11 20:40:07 +0900131 mIsSentenceSpellCheckSupported = true;
satok9b3855b2011-11-02 17:01:28 +0900132 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700133
134 // Restore SpellCheckSpans in pool
135 for (int i = 0; i < mLength; i++) {
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700136 mIds[i] = -1;
137 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700138 mLength = 0;
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700139
Gilles Debunne249d1e82011-12-12 20:06:29 -0800140 // Remove existing misspelled SuggestionSpans
141 mTextView.removeMisspelledSpans((Editable) mTextView.getText());
satok85894742012-04-20 17:54:51 +0900142 mSuggestionSpanCache.evictAll();
Gilles Debunne249d1e82011-12-12 20:06:29 -0800143 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700144
Gilles Debunne249d1e82011-12-12 20:06:29 -0800145 private void setLocale(Locale locale) {
146 mCurrentLocale = locale;
147
148 resetSession();
149
Satoshi Kataoka5bb4ee6d2012-12-05 22:25:48 +0900150 if (locale != null) {
151 // Change SpellParsers' wordIterator locale
152 mWordIterator = new WordIterator(locale);
153 }
Gilles Debunne249d1e82011-12-12 20:06:29 -0800154
155 // This class is the listener for locale change: warn other locale-aware objects
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700156 mTextView.onLocaleChanged();
Gilles Debunne6435a562011-08-04 21:22:30 -0700157 }
158
Gilles Debunne186aaf92011-09-16 14:26:12 -0700159 /**
160 * @return true if a spell checker session has successfully been created. Returns false if not,
161 * for instance when spell checking has been disabled in settings.
162 */
Gilles Debunne287d6c62011-10-05 18:22:11 -0700163 private boolean isSessionActive() {
Gilles Debunne186aaf92011-09-16 14:26:12 -0700164 return mSpellCheckerSession != null;
165 }
166
167 public void closeSession() {
168 if (mSpellCheckerSession != null) {
169 mSpellCheckerSession.close();
170 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700171
172 final int length = mSpellParsers.length;
173 for (int i = 0; i < length; i++) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700174 mSpellParsers[i].stop();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700175 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700176
177 if (mSpellRunnable != null) {
178 mTextView.removeCallbacks(mSpellRunnable);
179 }
Gilles Debunne186aaf92011-09-16 14:26:12 -0700180 }
181
Gilles Debunneb062e812011-09-27 14:58:37 -0700182 private int nextSpellCheckSpanIndex() {
183 for (int i = 0; i < mLength; i++) {
184 if (mIds[i] < 0) return i;
185 }
186
187 if (mLength == mSpellCheckSpans.length) {
188 final int newSize = mLength * 2;
Gilles Debunne6435a562011-08-04 21:22:30 -0700189 int[] newIds = new int[newSize];
190 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize];
Gilles Debunneb062e812011-09-27 14:58:37 -0700191 System.arraycopy(mIds, 0, newIds, 0, mLength);
192 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength);
Gilles Debunne6435a562011-08-04 21:22:30 -0700193 mIds = newIds;
194 mSpellCheckSpans = newSpellCheckSpans;
195 }
196
Gilles Debunneb062e812011-09-27 14:58:37 -0700197 mSpellCheckSpans[mLength] = new SpellCheckSpan();
Gilles Debunne6435a562011-08-04 21:22:30 -0700198 mLength++;
Gilles Debunneb062e812011-09-27 14:58:37 -0700199 return mLength - 1;
200 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700201
Gilles Debunnef6560302011-10-10 15:03:55 -0700202 private void addSpellCheckSpan(Editable editable, int start, int end) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700203 final int index = nextSpellCheckSpanIndex();
Gilles Debunne69865bd2012-05-09 11:12:03 -0700204 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[index];
205 editable.setSpan(spellCheckSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
206 spellCheckSpan.setSpellCheckInProgress(false);
Gilles Debunneb062e812011-09-27 14:58:37 -0700207 mIds[index] = mSpanSequenceCounter++;
Gilles Debunne6435a562011-08-04 21:22:30 -0700208 }
209
Gilles Debunne69865bd2012-05-09 11:12:03 -0700210 public void onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan) {
211 // Recycle any removed SpellCheckSpan (from this code or during text edition)
Gilles Debunne6435a562011-08-04 21:22:30 -0700212 for (int i = 0; i < mLength; i++) {
213 if (mSpellCheckSpans[i] == spellCheckSpan) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700214 mIds[i] = -1;
Gilles Debunne6435a562011-08-04 21:22:30 -0700215 return;
216 }
217 }
218 }
219
Gilles Debunne6435a562011-08-04 21:22:30 -0700220 public void onSelectionChanged() {
Gilles Debunneb062e812011-09-27 14:58:37 -0700221 spellCheck();
Gilles Debunne6435a562011-08-04 21:22:30 -0700222 }
223
Gilles Debunne287d6c62011-10-05 18:22:11 -0700224 public void spellCheck(int start, int end) {
satok85894742012-04-20 17:54:51 +0900225 if (DBG) {
226 Log.d(TAG, "Start spell-checking: " + start + ", " + end);
227 }
Satoshi Kataoka5bb4ee6d2012-12-05 22:25:48 +0900228 final Locale locale = mTextView.getSpellCheckerLocale();
Gilles Debunnec62589c2012-04-12 14:50:23 -0700229 final boolean isSessionActive = isSessionActive();
Satoshi Kataoka5bb4ee6d2012-12-05 22:25:48 +0900230 if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700231 setLocale(locale);
232 // Re-check the entire text
233 start = 0;
234 end = mTextView.getText().length();
Gilles Debunne249d1e82011-12-12 20:06:29 -0800235 } else {
236 final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
Gilles Debunnec62589c2012-04-12 14:50:23 -0700237 if (isSessionActive != spellCheckerActivated) {
Gilles Debunne249d1e82011-12-12 20:06:29 -0800238 // Spell checker has been turned of or off since last spellCheck
239 resetSession();
240 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700241 }
242
Gilles Debunnec62589c2012-04-12 14:50:23 -0700243 if (!isSessionActive) return;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700244
Gilles Debunnee9b82802011-10-27 14:38:27 -0700245 // Find first available SpellParser from pool
Gilles Debunne287d6c62011-10-05 18:22:11 -0700246 final int length = mSpellParsers.length;
247 for (int i = 0; i < length; i++) {
248 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunne249d1e82011-12-12 20:06:29 -0800249 if (spellParser.isFinished()) {
Gilles Debunne0249b432012-04-09 16:02:31 -0700250 spellParser.parse(start, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700251 return;
252 }
253 }
254
satok85894742012-04-20 17:54:51 +0900255 if (DBG) {
256 Log.d(TAG, "new spell parser.");
257 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700258 // No available parser found in pool, create a new one
259 SpellParser[] newSpellParsers = new SpellParser[length + 1];
260 System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
261 mSpellParsers = newSpellParsers;
262
263 SpellParser spellParser = new SpellParser();
264 mSpellParsers[length] = spellParser;
Gilles Debunne0249b432012-04-09 16:02:31 -0700265 spellParser.parse(start, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700266 }
267
268 private void spellCheck() {
Gilles Debunne0eea6682011-08-29 13:30:31 -0700269 if (mSpellCheckerSession == null) return;
Gilles Debunne99068472011-08-29 12:05:11 -0700270
Gilles Debunnef6560302011-10-10 15:03:55 -0700271 Editable editable = (Editable) mTextView.getText();
272 final int selectionStart = Selection.getSelectionStart(editable);
273 final int selectionEnd = Selection.getSelectionEnd(editable);
Gilles Debunne6435a562011-08-04 21:22:30 -0700274
275 TextInfo[] textInfos = new TextInfo[mLength];
276 int textInfosCount = 0;
277
278 for (int i = 0; i < mLength; i++) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700279 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
satok85894742012-04-20 17:54:51 +0900280 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue;
Gilles Debunne6435a562011-08-04 21:22:30 -0700281
Gilles Debunnef6560302011-10-10 15:03:55 -0700282 final int start = editable.getSpanStart(spellCheckSpan);
283 final int end = editable.getSpanEnd(spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700284
285 // Do not check this word if the user is currently editing it
satok85894742012-04-20 17:54:51 +0900286 final boolean isEditing;
287 if (mIsSentenceSpellCheckSupported) {
288 // Allow the overlap of the cursor and the first boundary of the spell check span
289 // no to skip the spell check of the following word because the
290 // following word will never be spell-checked even if the user finishes composing
291 isEditing = selectionEnd <= start || selectionStart > end;
292 } else {
293 isEditing = selectionEnd < start || selectionStart > end;
294 }
295 if (start >= 0 && end > start && isEditing) {
Gilles Debunne653d3a22011-12-07 10:35:59 -0800296 final String word = (editable instanceof SpannableStringBuilder) ?
297 ((SpannableStringBuilder) editable).substring(start, end) :
298 editable.subSequence(start, end).toString();
Gilles Debunneb062e812011-09-27 14:58:37 -0700299 spellCheckSpan.setSpellCheckInProgress(true);
Gilles Debunne6435a562011-08-04 21:22:30 -0700300 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
satoke1e87482012-04-18 16:52:44 +0900301 if (DBG) {
302 Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ")" + word
303 + ", cookie = " + mCookie + ", seq = "
304 + mIds[i] + ", sel start = " + selectionStart + ", sel end = "
305 + selectionEnd + ", start = " + start + ", end = " + end);
306 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700307 }
308 }
309
310 if (textInfosCount > 0) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700311 if (textInfosCount < textInfos.length) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700312 TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
313 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
314 textInfos = textInfosCopy;
315 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700316
satok88983582011-11-30 15:38:30 +0900317 if (mIsSentenceSpellCheckSupported) {
318 mSpellCheckerSession.getSentenceSuggestions(
319 textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE);
320 } else {
321 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
322 false /* TODO Set sequentialWords to true for initial spell check */);
323 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700324 }
325 }
326
satok88983582011-11-30 15:38:30 +0900327 private SpellCheckSpan onGetSuggestionsInternal(
328 SuggestionsInfo suggestionsInfo, int offset, int length) {
satok792ee0c2012-03-08 17:03:48 +0900329 if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) {
satok88983582011-11-30 15:38:30 +0900330 return null;
331 }
332 final Editable editable = (Editable) mTextView.getText();
333 final int sequenceNumber = suggestionsInfo.getSequence();
334 for (int k = 0; k < mLength; ++k) {
335 if (sequenceNumber == mIds[k]) {
336 final int attributes = suggestionsInfo.getSuggestionsAttributes();
337 final boolean isInDictionary =
338 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
339 final boolean looksLikeTypo =
340 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
341
342 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k];
343 //TODO: we need to change that rule for results from a sentence-level spell
344 // checker that will probably be in dictionary.
345 if (!isInDictionary && looksLikeTypo) {
346 createMisspelledSuggestionSpan(
347 editable, suggestionsInfo, spellCheckSpan, offset, length);
satokbec154c2012-05-11 15:49:14 +0900348 } else {
349 // Valid word -- isInDictionary || !looksLikeTypo
350 if (mIsSentenceSpellCheckSupported) {
351 // Allow the spell checker to remove existing misspelled span by
352 // overwriting the span over the same place
353 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
354 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
355 final int start;
356 final int end;
357 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
358 start = spellCheckSpanStart + offset;
359 end = start + length;
360 } else {
361 start = spellCheckSpanStart;
362 end = spellCheckSpanEnd;
363 }
364 if (spellCheckSpanStart >= 0 && spellCheckSpanEnd > spellCheckSpanStart
365 && end > start) {
366 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
367 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
368 if (tempSuggestionSpan != null) {
369 if (DBG) {
370 Log.i(TAG, "Remove existing misspelled span. "
371 + editable.subSequence(start, end));
372 }
373 editable.removeSpan(tempSuggestionSpan);
374 mSuggestionSpanCache.remove(key);
375 }
376 }
377 }
satok88983582011-11-30 15:38:30 +0900378 }
379 return spellCheckSpan;
380 }
381 }
382 return null;
satok0dc1f642011-11-18 11:27:10 +0900383 }
384
385 @Override
Gilles Debunne6435a562011-08-04 21:22:30 -0700386 public void onGetSuggestions(SuggestionsInfo[] results) {
satok88983582011-11-30 15:38:30 +0900387 final Editable editable = (Editable) mTextView.getText();
388 for (int i = 0; i < results.length; ++i) {
389 final SpellCheckSpan spellCheckSpan =
390 onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE);
391 if (spellCheckSpan != null) {
Gilles Debunne69865bd2012-05-09 11:12:03 -0700392 // onSpellCheckSpanRemoved will recycle this span in the pool
satok88983582011-11-30 15:38:30 +0900393 editable.removeSpan(spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700394 }
395 }
satok88983582011-11-30 15:38:30 +0900396 scheduleNewSpellCheck();
397 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700398
satok88983582011-11-30 15:38:30 +0900399 @Override
400 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
401 final Editable editable = (Editable) mTextView.getText();
402
403 for (int i = 0; i < results.length; ++i) {
404 final SentenceSuggestionsInfo ssi = results[i];
satok792ee0c2012-03-08 17:03:48 +0900405 if (ssi == null) {
406 continue;
407 }
satok88983582011-11-30 15:38:30 +0900408 SpellCheckSpan spellCheckSpan = null;
409 for (int j = 0; j < ssi.getSuggestionsCount(); ++j) {
410 final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j);
satok792ee0c2012-03-08 17:03:48 +0900411 if (suggestionsInfo == null) {
412 continue;
413 }
satok88983582011-11-30 15:38:30 +0900414 final int offset = ssi.getOffsetAt(j);
415 final int length = ssi.getLengthAt(j);
416 final SpellCheckSpan scs = onGetSuggestionsInternal(
417 suggestionsInfo, offset, length);
418 if (spellCheckSpan == null && scs != null) {
419 // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same
Gilles Debunne69865bd2012-05-09 11:12:03 -0700420 // SentenceSuggestionsInfo. Removal is deferred after this loop.
satok88983582011-11-30 15:38:30 +0900421 spellCheckSpan = scs;
422 }
423 }
424 if (spellCheckSpan != null) {
Gilles Debunne69865bd2012-05-09 11:12:03 -0700425 // onSpellCheckSpanRemoved will recycle this span in the pool
satok88983582011-11-30 15:38:30 +0900426 editable.removeSpan(spellCheckSpan);
427 }
428 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700429 scheduleNewSpellCheck();
430 }
431
432 private void scheduleNewSpellCheck() {
satok85894742012-04-20 17:54:51 +0900433 if (DBG) {
434 Log.i(TAG, "schedule new spell check.");
435 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700436 if (mSpellRunnable == null) {
437 mSpellRunnable = new Runnable() {
438 @Override
439 public void run() {
440 final int length = mSpellParsers.length;
441 for (int i = 0; i < length; i++) {
442 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunne249d1e82011-12-12 20:06:29 -0800443 if (!spellParser.isFinished()) {
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700444 spellParser.parse();
445 break; // run one spell parser at a time to bound running time
446 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700447 }
448 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700449 };
450 } else {
451 mTextView.removeCallbacks(mSpellRunnable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700452 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700453
454 mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
Gilles Debunne6435a562011-08-04 21:22:30 -0700455 }
456
Gilles Debunne8615ac92011-11-29 15:25:03 -0800457 private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
satok88983582011-11-30 15:38:30 +0900458 SpellCheckSpan spellCheckSpan, int offset, int length) {
459 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
460 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
461 if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart)
462 return; // span was removed in the meantime
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700463
satok88983582011-11-30 15:38:30 +0900464 final int start;
465 final int end;
466 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
467 start = spellCheckSpanStart + offset;
468 end = start + length;
469 } else {
470 start = spellCheckSpanStart;
471 end = spellCheckSpanEnd;
472 }
473
Gilles Debunne41347e92012-05-08 15:39:15 -0700474 final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
475 String[] suggestions;
476 if (suggestionsCount > 0) {
477 suggestions = new String[suggestionsCount];
478 for (int i = 0; i < suggestionsCount; i++) {
479 suggestions[i] = suggestionsInfo.getSuggestionAt(i);
480 }
481 } else {
482 suggestions = ArrayUtils.emptyArray(String.class);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700483 }
484
485 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
486 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
satok85894742012-04-20 17:54:51 +0900487 // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
488 // to share the logic of word level spell checker and sentence level spell checker
489 if (mIsSentenceSpellCheckSupported) {
Gilles Debunne41347e92012-05-08 15:39:15 -0700490 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
satok85894742012-04-20 17:54:51 +0900491 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
492 if (tempSuggestionSpan != null) {
493 if (DBG) {
494 Log.i(TAG, "Cached span on the same position is cleard. "
495 + editable.subSequence(start, end));
496 }
497 editable.removeSpan(tempSuggestionSpan);
498 }
499 mSuggestionSpanCache.put(key, suggestionSpan);
500 }
Gilles Debunnef6560302011-10-10 15:03:55 -0700501 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700502
Gilles Debunne961ebb92011-12-12 10:16:04 -0800503 mTextView.invalidateRegion(start, end, false /* No cursor involved */);
Gilles Debunne6435a562011-08-04 21:22:30 -0700504 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700505
506 private class SpellParser {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700507 private Object mRange = new Object();
508
Gilles Debunne0249b432012-04-09 16:02:31 -0700509 public void parse(int start, int end) {
satok37e169c2012-05-11 11:57:48 +0900510 final int max = mTextView.length();
511 final int parseEnd;
512 if (end > max) {
513 Log.w(TAG, "Parse invalid region, from " + start + " to " + end);
514 parseEnd = max;
515 } else {
516 parseEnd = end;
517 }
518 if (parseEnd > start) {
519 setRangeSpan((Editable) mTextView.getText(), start, parseEnd);
Gilles Debunne0249b432012-04-09 16:02:31 -0700520 parse();
521 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700522 }
523
Gilles Debunne249d1e82011-12-12 20:06:29 -0800524 public boolean isFinished() {
525 return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700526 }
527
Gilles Debunne249d1e82011-12-12 20:06:29 -0800528 public void stop() {
529 removeRangeSpan((Editable) mTextView.getText());
Gilles Debunnee9b82802011-10-27 14:38:27 -0700530 }
531
532 private void setRangeSpan(Editable editable, int start, int end) {
satok85894742012-04-20 17:54:51 +0900533 if (DBG) {
534 Log.d(TAG, "set next range span: " + start + ", " + end);
535 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700536 editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
537 }
538
539 private void removeRangeSpan(Editable editable) {
satok85894742012-04-20 17:54:51 +0900540 if (DBG) {
541 Log.d(TAG, "Remove range span." + editable.getSpanStart(editable)
542 + editable.getSpanEnd(editable));
543 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700544 editable.removeSpan(mRange);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700545 }
546
547 public void parse() {
Gilles Debunnef6560302011-10-10 15:03:55 -0700548 Editable editable = (Editable) mTextView.getText();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700549 // Iterate over the newly added text and schedule new SpellCheckSpans
satok24d146b2012-04-26 20:44:34 +0900550 final int start;
551 if (mIsSentenceSpellCheckSupported) {
552 // TODO: Find the start position of the sentence.
553 // Set span with the context
554 start = Math.max(
555 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH);
556 } else {
557 start = editable.getSpanStart(mRange);
558 }
559
Gilles Debunnef6560302011-10-10 15:03:55 -0700560 final int end = editable.getSpanEnd(mRange);
Gilles Debunne35199f52011-10-25 15:05:16 -0700561
562 int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700563 mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700564
565 // Move back to the beginning of the current word, if any
566 int wordStart = mWordIterator.preceding(start);
567 int wordEnd;
568 if (wordStart == BreakIterator.DONE) {
569 wordEnd = mWordIterator.following(start);
570 if (wordEnd != BreakIterator.DONE) {
571 wordStart = mWordIterator.getBeginning(wordEnd);
572 }
573 } else {
574 wordEnd = mWordIterator.getEnd(wordStart);
575 }
576 if (wordEnd == BreakIterator.DONE) {
satok85894742012-04-20 17:54:51 +0900577 if (DBG) {
578 Log.i(TAG, "No more spell check.");
579 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700580 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700581 return;
582 }
583
584 // We need to expand by one character because we want to include the spans that
585 // end/start at position start/end respectively.
Gilles Debunnef6560302011-10-10 15:03:55 -0700586 SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
587 SpellCheckSpan.class);
588 SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
589 SuggestionSpan.class);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700590
Gilles Debunne35199f52011-10-25 15:05:16 -0700591 int wordCount = 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700592 boolean scheduleOtherSpellCheck = false;
593
satok88983582011-11-30 15:38:30 +0900594 if (mIsSentenceSpellCheckSupported) {
satok88983582011-11-30 15:38:30 +0900595 if (wordIteratorWindowEnd < end) {
satok85894742012-04-20 17:54:51 +0900596 if (DBG) {
597 Log.i(TAG, "schedule other spell check.");
598 }
satok88983582011-11-30 15:38:30 +0900599 // Several batches needed on that region. Cut after last previous word
satok88983582011-11-30 15:38:30 +0900600 scheduleOtherSpellCheck = true;
satok88983582011-11-30 15:38:30 +0900601 }
satok85894742012-04-20 17:54:51 +0900602 int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd);
603 boolean correct = spellCheckEnd != BreakIterator.DONE;
satok88983582011-11-30 15:38:30 +0900604 if (correct) {
satok85894742012-04-20 17:54:51 +0900605 spellCheckEnd = mWordIterator.getEnd(spellCheckEnd);
606 correct = spellCheckEnd != BreakIterator.DONE;
satok88983582011-11-30 15:38:30 +0900607 }
608 if (!correct) {
satok85894742012-04-20 17:54:51 +0900609 if (DBG) {
610 Log.i(TAG, "Incorrect range span.");
611 }
612 removeRangeSpan(editable);
satok88983582011-11-30 15:38:30 +0900613 return;
614 }
satok85894742012-04-20 17:54:51 +0900615 do {
616 // TODO: Find the start position of the sentence.
617 int spellCheckStart = wordStart;
618 boolean createSpellCheckSpan = true;
619 // Cancel or merge overlapped spell check spans
620 for (int i = 0; i < mLength; ++i) {
621 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
622 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) {
623 continue;
624 }
625 final int spanStart = editable.getSpanStart(spellCheckSpan);
626 final int spanEnd = editable.getSpanEnd(spellCheckSpan);
627 if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) {
628 // No need to merge
629 continue;
630 }
631 if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) {
632 // There is a completely overlapped spell check span
633 // skip this span
634 createSpellCheckSpan = false;
635 if (DBG) {
636 Log.i(TAG, "The range is overrapped. Skip spell check.");
637 }
638 break;
639 }
Gilles Debunne69865bd2012-05-09 11:12:03 -0700640 // This spellCheckSpan is replaced by the one we are creating
641 editable.removeSpan(spellCheckSpan);
satok85894742012-04-20 17:54:51 +0900642 spellCheckStart = Math.min(spanStart, spellCheckStart);
643 spellCheckEnd = Math.max(spanEnd, spellCheckEnd);
644 }
645
646 if (DBG) {
647 Log.d(TAG, "addSpellCheckSpan: "
648 + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart
649 + ", next = " + scheduleOtherSpellCheck + "\n"
650 + editable.subSequence(spellCheckStart, spellCheckEnd));
651 }
652
653 // Stop spell checking when there are no characters in the range.
654 if (spellCheckEnd < start) {
655 break;
656 }
satoka4c82c12012-05-09 11:20:17 +0900657 if (spellCheckEnd <= spellCheckStart) {
satok37e169c2012-05-11 11:57:48 +0900658 Log.w(TAG, "Trying to spellcheck invalid region, from "
659 + start + " to " + end);
satoka4c82c12012-05-09 11:20:17 +0900660 break;
661 }
satok85894742012-04-20 17:54:51 +0900662 if (createSpellCheckSpan) {
663 addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd);
664 }
665 } while (false);
666 wordStart = spellCheckEnd;
satok88983582011-11-30 15:38:30 +0900667 } else {
668 while (wordStart <= end) {
669 if (wordEnd >= start && wordEnd > wordStart) {
670 if (wordCount >= MAX_NUMBER_OF_WORDS) {
671 scheduleOtherSpellCheck = true;
672 break;
673 }
674 // A new word has been created across the interval boundaries with this
675 // edit. The previous spans (that ended on start / started on end) are
676 // not valid anymore and must be removed.
677 if (wordStart < start && wordEnd > start) {
678 removeSpansAt(editable, start, spellCheckSpans);
679 removeSpansAt(editable, start, suggestionSpans);
680 }
681
682 if (wordStart < end && wordEnd > end) {
683 removeSpansAt(editable, end, spellCheckSpans);
684 removeSpansAt(editable, end, suggestionSpans);
685 }
686
687 // Do not create new boundary spans if they already exist
688 boolean createSpellCheckSpan = true;
689 if (wordEnd == start) {
690 for (int i = 0; i < spellCheckSpans.length; i++) {
691 final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
692 if (spanEnd == start) {
693 createSpellCheckSpan = false;
694 break;
695 }
696 }
697 }
698
699 if (wordStart == end) {
700 for (int i = 0; i < spellCheckSpans.length; i++) {
701 final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
702 if (spanStart == end) {
703 createSpellCheckSpan = false;
704 break;
705 }
706 }
707 }
708
709 if (createSpellCheckSpan) {
710 addSpellCheckSpan(editable, wordStart, wordEnd);
711 }
712 wordCount++;
713 }
714
715 // iterate word by word
716 int originalWordEnd = wordEnd;
717 wordEnd = mWordIterator.following(wordEnd);
718 if ((wordIteratorWindowEnd < end) &&
719 (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
720 wordIteratorWindowEnd =
721 Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
722 mWordIterator.setCharSequence(
723 editable, originalWordEnd, wordIteratorWindowEnd);
724 wordEnd = mWordIterator.following(originalWordEnd);
725 }
726 if (wordEnd == BreakIterator.DONE) break;
727 wordStart = mWordIterator.getBeginning(wordEnd);
728 if (wordStart == BreakIterator.DONE) {
Gilles Debunne35199f52011-10-25 15:05:16 -0700729 break;
730 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700731 }
732 }
733
734 if (scheduleOtherSpellCheck) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700735 // Update range span: start new spell check from last wordStart
736 setRangeSpan(editable, wordStart, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700737 } else {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700738 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700739 }
740
741 spellCheck();
742 }
743
Gilles Debunnef6560302011-10-10 15:03:55 -0700744 private <T> void removeSpansAt(Editable editable, int offset, T[] spans) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700745 final int length = spans.length;
746 for (int i = 0; i < length; i++) {
747 final T span = spans[i];
Gilles Debunnef6560302011-10-10 15:03:55 -0700748 final int start = editable.getSpanStart(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700749 if (start > offset) continue;
Gilles Debunnef6560302011-10-10 15:03:55 -0700750 final int end = editable.getSpanEnd(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700751 if (end < offset) continue;
Gilles Debunnef6560302011-10-10 15:03:55 -0700752 editable.removeSpan(span);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700753 }
754 }
755 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700756}