blob: 3592687d1f102c9983b9740735536b9836dc05fb [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;
22import android.text.Spanned;
satok85894742012-04-20 17:54:51 +090023import android.text.TextUtils;
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;
satoke1e87482012-04-18 16:52:44 +090027import android.util.Log;
satok85894742012-04-20 17:54:51 +090028import android.util.LruCache;
satokd404fe12012-02-22 06:38:18 +090029import android.view.textservice.SentenceSuggestionsInfo;
Gilles Debunne6435a562011-08-04 21:22:30 -070030import android.view.textservice.SpellCheckerSession;
31import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
32import android.view.textservice.SuggestionsInfo;
33import android.view.textservice.TextInfo;
34import android.view.textservice.TextServicesManager;
35
36import com.android.internal.util.ArrayUtils;
Adam Lesinski776abc22014-03-07 11:30:59 -050037import com.android.internal.util.GrowingArrayUtils;
Gilles Debunne6435a562011-08-04 21:22:30 -070038
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/**
Yohei Yukawa5d6b6f22014-06-25 19:46:47 +090044 * Helper class for TextView. Bridge between the TextView and the Dictionary service.
Gilles Debunne6435a562011-08-04 21:22:30 -070045 *
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
Yohei Yukawa5d6b6f22014-06-25 19:46:47 +090085 // Parsers on chunk of text, cutting text into words that will be checked
Gilles Debunne287d6c62011-10-05 18:22:11 -070086 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
Adam Lesinski776abc22014-03-07 11:30:59 -0500108 final int size = 1;
109 mIds = ArrayUtils.newUnpaddedIntArray(size);
110 mSpellCheckSpans = new SpellCheckSpan[mIds.length];
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
Adam Lesinski776abc22014-03-07 11:30:59 -0500187 mIds = GrowingArrayUtils.append(mIds, mLength, 0);
188 mSpellCheckSpans = GrowingArrayUtils.append(
189 mSpellCheckSpans, mLength, new SpellCheckSpan());
Gilles Debunne6435a562011-08-04 21:22:30 -0700190 mLength++;
Gilles Debunneb062e812011-09-27 14:58:37 -0700191 return mLength - 1;
192 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700193
Gilles Debunnef6560302011-10-10 15:03:55 -0700194 private void addSpellCheckSpan(Editable editable, int start, int end) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700195 final int index = nextSpellCheckSpanIndex();
Gilles Debunne69865bd2012-05-09 11:12:03 -0700196 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[index];
197 editable.setSpan(spellCheckSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
198 spellCheckSpan.setSpellCheckInProgress(false);
Gilles Debunneb062e812011-09-27 14:58:37 -0700199 mIds[index] = mSpanSequenceCounter++;
Gilles Debunne6435a562011-08-04 21:22:30 -0700200 }
201
Gilles Debunne69865bd2012-05-09 11:12:03 -0700202 public void onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan) {
203 // Recycle any removed SpellCheckSpan (from this code or during text edition)
Gilles Debunne6435a562011-08-04 21:22:30 -0700204 for (int i = 0; i < mLength; i++) {
205 if (mSpellCheckSpans[i] == spellCheckSpan) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700206 mIds[i] = -1;
Gilles Debunne6435a562011-08-04 21:22:30 -0700207 return;
208 }
209 }
210 }
211
Gilles Debunne6435a562011-08-04 21:22:30 -0700212 public void onSelectionChanged() {
Gilles Debunneb062e812011-09-27 14:58:37 -0700213 spellCheck();
Gilles Debunne6435a562011-08-04 21:22:30 -0700214 }
215
Gilles Debunne287d6c62011-10-05 18:22:11 -0700216 public void spellCheck(int start, int end) {
satok85894742012-04-20 17:54:51 +0900217 if (DBG) {
218 Log.d(TAG, "Start spell-checking: " + start + ", " + end);
219 }
Satoshi Kataoka5bb4ee6d2012-12-05 22:25:48 +0900220 final Locale locale = mTextView.getSpellCheckerLocale();
Gilles Debunnec62589c2012-04-12 14:50:23 -0700221 final boolean isSessionActive = isSessionActive();
Satoshi Kataoka5bb4ee6d2012-12-05 22:25:48 +0900222 if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700223 setLocale(locale);
224 // Re-check the entire text
225 start = 0;
226 end = mTextView.getText().length();
Gilles Debunne249d1e82011-12-12 20:06:29 -0800227 } else {
228 final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
Gilles Debunnec62589c2012-04-12 14:50:23 -0700229 if (isSessionActive != spellCheckerActivated) {
Gilles Debunne249d1e82011-12-12 20:06:29 -0800230 // Spell checker has been turned of or off since last spellCheck
231 resetSession();
232 }
Gilles Debunne9d8d3f12011-10-13 12:15:10 -0700233 }
234
Gilles Debunnec62589c2012-04-12 14:50:23 -0700235 if (!isSessionActive) return;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700236
Gilles Debunnee9b82802011-10-27 14:38:27 -0700237 // Find first available SpellParser from pool
Gilles Debunne287d6c62011-10-05 18:22:11 -0700238 final int length = mSpellParsers.length;
239 for (int i = 0; i < length; i++) {
240 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunne249d1e82011-12-12 20:06:29 -0800241 if (spellParser.isFinished()) {
Gilles Debunne0249b432012-04-09 16:02:31 -0700242 spellParser.parse(start, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700243 return;
244 }
245 }
246
satok85894742012-04-20 17:54:51 +0900247 if (DBG) {
248 Log.d(TAG, "new spell parser.");
249 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700250 // No available parser found in pool, create a new one
251 SpellParser[] newSpellParsers = new SpellParser[length + 1];
252 System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
253 mSpellParsers = newSpellParsers;
254
255 SpellParser spellParser = new SpellParser();
256 mSpellParsers[length] = spellParser;
Gilles Debunne0249b432012-04-09 16:02:31 -0700257 spellParser.parse(start, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700258 }
259
260 private void spellCheck() {
Gilles Debunne0eea6682011-08-29 13:30:31 -0700261 if (mSpellCheckerSession == null) return;
Gilles Debunne99068472011-08-29 12:05:11 -0700262
Gilles Debunnef6560302011-10-10 15:03:55 -0700263 Editable editable = (Editable) mTextView.getText();
264 final int selectionStart = Selection.getSelectionStart(editable);
265 final int selectionEnd = Selection.getSelectionEnd(editable);
Gilles Debunne6435a562011-08-04 21:22:30 -0700266
267 TextInfo[] textInfos = new TextInfo[mLength];
268 int textInfosCount = 0;
269
270 for (int i = 0; i < mLength; i++) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700271 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
satok85894742012-04-20 17:54:51 +0900272 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue;
Gilles Debunne6435a562011-08-04 21:22:30 -0700273
Gilles Debunnef6560302011-10-10 15:03:55 -0700274 final int start = editable.getSpanStart(spellCheckSpan);
275 final int end = editable.getSpanEnd(spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700276
277 // Do not check this word if the user is currently editing it
satok85894742012-04-20 17:54:51 +0900278 final boolean isEditing;
Raph Levienb1fef112014-09-25 12:50:16 -0700279
280 // Defer spell check when typing a word with an interior apostrophe.
281 // TODO: a better solution to this would be to make the word
282 // iterator locale-sensitive and include the apostrophe in
283 // languages that use it (such as English).
284 final boolean apostrophe = (selectionStart == end + 1 && editable.charAt(end) == '\'');
satok85894742012-04-20 17:54:51 +0900285 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
Raph Levienb1fef112014-09-25 12:50:16 -0700289 isEditing = !apostrophe && (selectionEnd <= start || selectionStart > end);
satok85894742012-04-20 17:54:51 +0900290 } else {
Raph Levienb1fef112014-09-25 12:50:16 -0700291 isEditing = !apostrophe && (selectionEnd < start || selectionStart > end);
satok85894742012-04-20 17:54:51 +0900292 }
293 if (start >= 0 && end > start && isEditing) {
Gilles Debunneb062e812011-09-27 14:58:37 -0700294 spellCheckSpan.setSpellCheckInProgress(true);
Yohei Yukawa5d6b6f22014-06-25 19:46:47 +0900295 final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]);
296 textInfos[textInfosCount++] = textInfo;
satoke1e87482012-04-18 16:52:44 +0900297 if (DBG) {
Yohei Yukawa5d6b6f22014-06-25 19:46:47 +0900298 Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ") text = "
299 + textInfo.getSequence() + ", cookie = " + mCookie + ", seq = "
satoke1e87482012-04-18 16:52:44 +0900300 + mIds[i] + ", sel start = " + selectionStart + ", sel end = "
301 + selectionEnd + ", start = " + start + ", end = " + end);
302 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700303 }
304 }
305
306 if (textInfosCount > 0) {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700307 if (textInfosCount < textInfos.length) {
Gilles Debunne6435a562011-08-04 21:22:30 -0700308 TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
309 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
310 textInfos = textInfosCopy;
311 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700312
satok88983582011-11-30 15:38:30 +0900313 if (mIsSentenceSpellCheckSupported) {
314 mSpellCheckerSession.getSentenceSuggestions(
315 textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE);
316 } else {
317 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
318 false /* TODO Set sequentialWords to true for initial spell check */);
319 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700320 }
321 }
322
satok88983582011-11-30 15:38:30 +0900323 private SpellCheckSpan onGetSuggestionsInternal(
324 SuggestionsInfo suggestionsInfo, int offset, int length) {
satok792ee0c2012-03-08 17:03:48 +0900325 if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) {
satok88983582011-11-30 15:38:30 +0900326 return null;
327 }
328 final Editable editable = (Editable) mTextView.getText();
329 final int sequenceNumber = suggestionsInfo.getSequence();
330 for (int k = 0; k < mLength; ++k) {
331 if (sequenceNumber == mIds[k]) {
332 final int attributes = suggestionsInfo.getSuggestionsAttributes();
333 final boolean isInDictionary =
334 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
335 final boolean looksLikeTypo =
336 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
337
338 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k];
339 //TODO: we need to change that rule for results from a sentence-level spell
340 // checker that will probably be in dictionary.
341 if (!isInDictionary && looksLikeTypo) {
342 createMisspelledSuggestionSpan(
343 editable, suggestionsInfo, spellCheckSpan, offset, length);
satokbec154c2012-05-11 15:49:14 +0900344 } else {
345 // Valid word -- isInDictionary || !looksLikeTypo
346 if (mIsSentenceSpellCheckSupported) {
347 // Allow the spell checker to remove existing misspelled span by
348 // overwriting the span over the same place
349 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
350 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
351 final int start;
352 final int end;
353 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
354 start = spellCheckSpanStart + offset;
355 end = start + length;
356 } else {
357 start = spellCheckSpanStart;
358 end = spellCheckSpanEnd;
359 }
360 if (spellCheckSpanStart >= 0 && spellCheckSpanEnd > spellCheckSpanStart
361 && end > start) {
362 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
363 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
364 if (tempSuggestionSpan != null) {
365 if (DBG) {
366 Log.i(TAG, "Remove existing misspelled span. "
367 + editable.subSequence(start, end));
368 }
369 editable.removeSpan(tempSuggestionSpan);
370 mSuggestionSpanCache.remove(key);
371 }
372 }
373 }
satok88983582011-11-30 15:38:30 +0900374 }
375 return spellCheckSpan;
376 }
377 }
378 return null;
satok0dc1f642011-11-18 11:27:10 +0900379 }
380
381 @Override
Gilles Debunne6435a562011-08-04 21:22:30 -0700382 public void onGetSuggestions(SuggestionsInfo[] results) {
satok88983582011-11-30 15:38:30 +0900383 final Editable editable = (Editable) mTextView.getText();
384 for (int i = 0; i < results.length; ++i) {
385 final SpellCheckSpan spellCheckSpan =
386 onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE);
387 if (spellCheckSpan != null) {
Gilles Debunne69865bd2012-05-09 11:12:03 -0700388 // onSpellCheckSpanRemoved will recycle this span in the pool
satok88983582011-11-30 15:38:30 +0900389 editable.removeSpan(spellCheckSpan);
Gilles Debunne6435a562011-08-04 21:22:30 -0700390 }
391 }
satok88983582011-11-30 15:38:30 +0900392 scheduleNewSpellCheck();
393 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700394
satok88983582011-11-30 15:38:30 +0900395 @Override
396 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
397 final Editable editable = (Editable) mTextView.getText();
398
399 for (int i = 0; i < results.length; ++i) {
400 final SentenceSuggestionsInfo ssi = results[i];
satok792ee0c2012-03-08 17:03:48 +0900401 if (ssi == null) {
402 continue;
403 }
satok88983582011-11-30 15:38:30 +0900404 SpellCheckSpan spellCheckSpan = null;
405 for (int j = 0; j < ssi.getSuggestionsCount(); ++j) {
406 final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j);
satok792ee0c2012-03-08 17:03:48 +0900407 if (suggestionsInfo == null) {
408 continue;
409 }
satok88983582011-11-30 15:38:30 +0900410 final int offset = ssi.getOffsetAt(j);
411 final int length = ssi.getLengthAt(j);
412 final SpellCheckSpan scs = onGetSuggestionsInternal(
413 suggestionsInfo, offset, length);
414 if (spellCheckSpan == null && scs != null) {
415 // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same
Gilles Debunne69865bd2012-05-09 11:12:03 -0700416 // SentenceSuggestionsInfo. Removal is deferred after this loop.
satok88983582011-11-30 15:38:30 +0900417 spellCheckSpan = scs;
418 }
419 }
420 if (spellCheckSpan != null) {
Gilles Debunne69865bd2012-05-09 11:12:03 -0700421 // onSpellCheckSpanRemoved will recycle this span in the pool
satok88983582011-11-30 15:38:30 +0900422 editable.removeSpan(spellCheckSpan);
423 }
424 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700425 scheduleNewSpellCheck();
426 }
427
428 private void scheduleNewSpellCheck() {
satok85894742012-04-20 17:54:51 +0900429 if (DBG) {
430 Log.i(TAG, "schedule new spell check.");
431 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700432 if (mSpellRunnable == null) {
433 mSpellRunnable = new Runnable() {
434 @Override
435 public void run() {
436 final int length = mSpellParsers.length;
437 for (int i = 0; i < length; i++) {
438 final SpellParser spellParser = mSpellParsers[i];
Gilles Debunne249d1e82011-12-12 20:06:29 -0800439 if (!spellParser.isFinished()) {
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700440 spellParser.parse();
441 break; // run one spell parser at a time to bound running time
442 }
Gilles Debunne35199f52011-10-25 15:05:16 -0700443 }
444 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700445 };
446 } else {
447 mTextView.removeCallbacks(mSpellRunnable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700448 }
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700449
450 mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
Gilles Debunne6435a562011-08-04 21:22:30 -0700451 }
452
Gilles Debunne8615ac92011-11-29 15:25:03 -0800453 private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
satok88983582011-11-30 15:38:30 +0900454 SpellCheckSpan spellCheckSpan, int offset, int length) {
455 final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
456 final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
457 if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart)
458 return; // span was removed in the meantime
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700459
satok88983582011-11-30 15:38:30 +0900460 final int start;
461 final int end;
462 if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
463 start = spellCheckSpanStart + offset;
464 end = start + length;
465 } else {
466 start = spellCheckSpanStart;
467 end = spellCheckSpanEnd;
468 }
469
Gilles Debunne41347e92012-05-08 15:39:15 -0700470 final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
471 String[] suggestions;
472 if (suggestionsCount > 0) {
473 suggestions = new String[suggestionsCount];
474 for (int i = 0; i < suggestionsCount; i++) {
475 suggestions[i] = suggestionsInfo.getSuggestionAt(i);
476 }
477 } else {
478 suggestions = ArrayUtils.emptyArray(String.class);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700479 }
480
481 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
482 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
satok85894742012-04-20 17:54:51 +0900483 // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
484 // to share the logic of word level spell checker and sentence level spell checker
485 if (mIsSentenceSpellCheckSupported) {
Gilles Debunne41347e92012-05-08 15:39:15 -0700486 final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
satok85894742012-04-20 17:54:51 +0900487 final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
488 if (tempSuggestionSpan != null) {
489 if (DBG) {
490 Log.i(TAG, "Cached span on the same position is cleard. "
491 + editable.subSequence(start, end));
492 }
493 editable.removeSpan(tempSuggestionSpan);
494 }
495 mSuggestionSpanCache.put(key, suggestionSpan);
496 }
Gilles Debunnef6560302011-10-10 15:03:55 -0700497 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Gilles Debunne176cd0d2011-09-29 16:37:27 -0700498
Gilles Debunne961ebb92011-12-12 10:16:04 -0800499 mTextView.invalidateRegion(start, end, false /* No cursor involved */);
Gilles Debunne6435a562011-08-04 21:22:30 -0700500 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700501
502 private class SpellParser {
Gilles Debunne287d6c62011-10-05 18:22:11 -0700503 private Object mRange = new Object();
504
Gilles Debunne0249b432012-04-09 16:02:31 -0700505 public void parse(int start, int end) {
satok37e169c2012-05-11 11:57:48 +0900506 final int max = mTextView.length();
507 final int parseEnd;
508 if (end > max) {
509 Log.w(TAG, "Parse invalid region, from " + start + " to " + end);
510 parseEnd = max;
511 } else {
512 parseEnd = end;
513 }
514 if (parseEnd > start) {
515 setRangeSpan((Editable) mTextView.getText(), start, parseEnd);
Gilles Debunne0249b432012-04-09 16:02:31 -0700516 parse();
517 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700518 }
519
Gilles Debunne249d1e82011-12-12 20:06:29 -0800520 public boolean isFinished() {
521 return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700522 }
523
Gilles Debunne249d1e82011-12-12 20:06:29 -0800524 public void stop() {
525 removeRangeSpan((Editable) mTextView.getText());
Gilles Debunnee9b82802011-10-27 14:38:27 -0700526 }
527
528 private void setRangeSpan(Editable editable, int start, int end) {
satok85894742012-04-20 17:54:51 +0900529 if (DBG) {
530 Log.d(TAG, "set next range span: " + start + ", " + end);
531 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700532 editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
533 }
534
535 private void removeRangeSpan(Editable editable) {
satok85894742012-04-20 17:54:51 +0900536 if (DBG) {
537 Log.d(TAG, "Remove range span." + editable.getSpanStart(editable)
538 + editable.getSpanEnd(editable));
539 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700540 editable.removeSpan(mRange);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700541 }
542
543 public void parse() {
Gilles Debunnef6560302011-10-10 15:03:55 -0700544 Editable editable = (Editable) mTextView.getText();
Gilles Debunne287d6c62011-10-05 18:22:11 -0700545 // Iterate over the newly added text and schedule new SpellCheckSpans
satok24d146b2012-04-26 20:44:34 +0900546 final int start;
547 if (mIsSentenceSpellCheckSupported) {
548 // TODO: Find the start position of the sentence.
549 // Set span with the context
550 start = Math.max(
551 0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH);
552 } else {
553 start = editable.getSpanStart(mRange);
554 }
555
Gilles Debunnef6560302011-10-10 15:03:55 -0700556 final int end = editable.getSpanEnd(mRange);
Gilles Debunne35199f52011-10-25 15:05:16 -0700557
558 int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
Gilles Debunnebe5f49f2011-10-25 15:05:16 -0700559 mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700560
561 // Move back to the beginning of the current word, if any
562 int wordStart = mWordIterator.preceding(start);
563 int wordEnd;
564 if (wordStart == BreakIterator.DONE) {
565 wordEnd = mWordIterator.following(start);
566 if (wordEnd != BreakIterator.DONE) {
567 wordStart = mWordIterator.getBeginning(wordEnd);
568 }
569 } else {
570 wordEnd = mWordIterator.getEnd(wordStart);
571 }
572 if (wordEnd == BreakIterator.DONE) {
satok85894742012-04-20 17:54:51 +0900573 if (DBG) {
574 Log.i(TAG, "No more spell check.");
575 }
Gilles Debunnee9b82802011-10-27 14:38:27 -0700576 removeRangeSpan(editable);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700577 return;
578 }
579
580 // We need to expand by one character because we want to include the spans that
581 // end/start at position start/end respectively.
Gilles Debunnef6560302011-10-10 15:03:55 -0700582 SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
583 SpellCheckSpan.class);
584 SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
585 SuggestionSpan.class);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700586
Gilles Debunne35199f52011-10-25 15:05:16 -0700587 int wordCount = 0;
Gilles Debunne287d6c62011-10-05 18:22:11 -0700588 boolean scheduleOtherSpellCheck = false;
589
satok88983582011-11-30 15:38:30 +0900590 if (mIsSentenceSpellCheckSupported) {
satok88983582011-11-30 15:38:30 +0900591 if (wordIteratorWindowEnd < end) {
satok85894742012-04-20 17:54:51 +0900592 if (DBG) {
593 Log.i(TAG, "schedule other spell check.");
594 }
satok88983582011-11-30 15:38:30 +0900595 // Several batches needed on that region. Cut after last previous word
satok88983582011-11-30 15:38:30 +0900596 scheduleOtherSpellCheck = true;
satok88983582011-11-30 15:38:30 +0900597 }
satok85894742012-04-20 17:54:51 +0900598 int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd);
599 boolean correct = spellCheckEnd != BreakIterator.DONE;
satok88983582011-11-30 15:38:30 +0900600 if (correct) {
satok85894742012-04-20 17:54:51 +0900601 spellCheckEnd = mWordIterator.getEnd(spellCheckEnd);
602 correct = spellCheckEnd != BreakIterator.DONE;
satok88983582011-11-30 15:38:30 +0900603 }
604 if (!correct) {
satok85894742012-04-20 17:54:51 +0900605 if (DBG) {
606 Log.i(TAG, "Incorrect range span.");
607 }
608 removeRangeSpan(editable);
satok88983582011-11-30 15:38:30 +0900609 return;
610 }
satok85894742012-04-20 17:54:51 +0900611 do {
612 // TODO: Find the start position of the sentence.
613 int spellCheckStart = wordStart;
614 boolean createSpellCheckSpan = true;
615 // Cancel or merge overlapped spell check spans
616 for (int i = 0; i < mLength; ++i) {
617 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
618 if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) {
619 continue;
620 }
621 final int spanStart = editable.getSpanStart(spellCheckSpan);
622 final int spanEnd = editable.getSpanEnd(spellCheckSpan);
623 if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) {
624 // No need to merge
625 continue;
626 }
627 if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) {
628 // There is a completely overlapped spell check span
629 // skip this span
630 createSpellCheckSpan = false;
631 if (DBG) {
632 Log.i(TAG, "The range is overrapped. Skip spell check.");
633 }
634 break;
635 }
Gilles Debunne69865bd2012-05-09 11:12:03 -0700636 // This spellCheckSpan is replaced by the one we are creating
637 editable.removeSpan(spellCheckSpan);
satok85894742012-04-20 17:54:51 +0900638 spellCheckStart = Math.min(spanStart, spellCheckStart);
639 spellCheckEnd = Math.max(spanEnd, spellCheckEnd);
640 }
641
642 if (DBG) {
643 Log.d(TAG, "addSpellCheckSpan: "
644 + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart
645 + ", next = " + scheduleOtherSpellCheck + "\n"
646 + editable.subSequence(spellCheckStart, spellCheckEnd));
647 }
648
649 // Stop spell checking when there are no characters in the range.
650 if (spellCheckEnd < start) {
651 break;
652 }
satoka4c82c12012-05-09 11:20:17 +0900653 if (spellCheckEnd <= spellCheckStart) {
satok37e169c2012-05-11 11:57:48 +0900654 Log.w(TAG, "Trying to spellcheck invalid region, from "
655 + start + " to " + end);
satoka4c82c12012-05-09 11:20:17 +0900656 break;
657 }
satok85894742012-04-20 17:54:51 +0900658 if (createSpellCheckSpan) {
659 addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd);
660 }
661 } while (false);
662 wordStart = spellCheckEnd;
satok88983582011-11-30 15:38:30 +0900663 } else {
664 while (wordStart <= end) {
665 if (wordEnd >= start && wordEnd > wordStart) {
666 if (wordCount >= MAX_NUMBER_OF_WORDS) {
667 scheduleOtherSpellCheck = true;
668 break;
669 }
670 // A new word has been created across the interval boundaries with this
671 // edit. The previous spans (that ended on start / started on end) are
672 // not valid anymore and must be removed.
673 if (wordStart < start && wordEnd > start) {
674 removeSpansAt(editable, start, spellCheckSpans);
675 removeSpansAt(editable, start, suggestionSpans);
676 }
677
678 if (wordStart < end && wordEnd > end) {
679 removeSpansAt(editable, end, spellCheckSpans);
680 removeSpansAt(editable, end, suggestionSpans);
681 }
682
683 // Do not create new boundary spans if they already exist
684 boolean createSpellCheckSpan = true;
685 if (wordEnd == start) {
686 for (int i = 0; i < spellCheckSpans.length; i++) {
687 final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
688 if (spanEnd == start) {
689 createSpellCheckSpan = false;
690 break;
691 }
692 }
693 }
694
695 if (wordStart == end) {
696 for (int i = 0; i < spellCheckSpans.length; i++) {
697 final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
698 if (spanStart == end) {
699 createSpellCheckSpan = false;
700 break;
701 }
702 }
703 }
704
705 if (createSpellCheckSpan) {
706 addSpellCheckSpan(editable, wordStart, wordEnd);
707 }
708 wordCount++;
709 }
710
711 // iterate word by word
712 int originalWordEnd = wordEnd;
713 wordEnd = mWordIterator.following(wordEnd);
714 if ((wordIteratorWindowEnd < end) &&
715 (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
716 wordIteratorWindowEnd =
717 Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
718 mWordIterator.setCharSequence(
719 editable, originalWordEnd, wordIteratorWindowEnd);
720 wordEnd = mWordIterator.following(originalWordEnd);
721 }
722 if (wordEnd == BreakIterator.DONE) break;
723 wordStart = mWordIterator.getBeginning(wordEnd);
724 if (wordStart == BreakIterator.DONE) {
Gilles Debunne35199f52011-10-25 15:05:16 -0700725 break;
726 }
Gilles Debunne287d6c62011-10-05 18:22:11 -0700727 }
728 }
729
Jay Shraunerea4adf22014-02-07 13:28:59 -0800730 if (scheduleOtherSpellCheck && wordStart <= end) {
Gilles Debunnee9b82802011-10-27 14:38:27 -0700731 // Update range span: start new spell check from last wordStart
732 setRangeSpan(editable, wordStart, end);
Gilles Debunne287d6c62011-10-05 18:22:11 -0700733 } else {
Jay Shraunerea4adf22014-02-07 13:28:59 -0800734 if (DBG && scheduleOtherSpellCheck) {
735 Log.w(TAG, "Trying to schedule spellcheck for invalid region, from "
736 + wordStart + " to " + end);
737 }
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 }
Satoshi Kataokad7429c12013-06-05 16:30:23 +0900756
757 public static boolean haveWordBoundariesChanged(final Editable editable, final int start,
758 final int end, final int spanStart, final int spanEnd) {
759 final boolean haveWordBoundariesChanged;
760 if (spanEnd != start && spanStart != end) {
761 haveWordBoundariesChanged = true;
762 if (DBG) {
763 Log.d(TAG, "(1) Text inside the span has been modified. Remove.");
764 }
765 } else if (spanEnd == start && start < editable.length()) {
766 final int codePoint = Character.codePointAt(editable, start);
767 haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
768 if (DBG) {
769 Log.d(TAG, "(2) Characters have been appended to the spanned text. "
770 + (haveWordBoundariesChanged ? "Remove.<" : "Keep. <") + (char)(codePoint)
771 + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
772 + start);
773 }
774 } else if (spanStart == end && end > 0) {
775 final int codePoint = Character.codePointBefore(editable, end);
776 haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
777 if (DBG) {
778 Log.d(TAG, "(3) Characters have been prepended to the spanned text. "
779 + (haveWordBoundariesChanged ? "Remove.<" : "Keep.<") + (char)(codePoint)
780 + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
781 + end);
782 }
783 } else {
784 if (DBG) {
785 Log.d(TAG, "(4) Characters adjacent to the spanned text were deleted. Keep.");
786 }
787 haveWordBoundariesChanged = false;
788 }
789 return haveWordBoundariesChanged;
790 }
Gilles Debunne6435a562011-08-04 21:22:30 -0700791}