SpellChecker.java revision b10d396f2e1d0329013f5376bd486621bd177bc8
1/* 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 */ 16 17package android.widget; 18 19import android.content.Context; 20import android.text.Editable; 21import android.text.Selection; 22import android.text.Spanned; 23import android.text.method.WordIterator; 24import android.text.style.SpellCheckSpan; 25import android.text.style.SuggestionSpan; 26import android.view.textservice.SpellCheckerSession; 27import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; 28import android.view.textservice.SuggestionsInfo; 29import android.view.textservice.TextInfo; 30import android.view.textservice.TextServicesManager; 31 32import com.android.internal.util.ArrayUtils; 33 34import java.text.BreakIterator; 35 36 37/** 38 * Helper class for TextView. Bridge between the TextView and the Dictionnary service. 39 * 40 * @hide 41 */ 42public class SpellChecker implements SpellCheckerSessionListener { 43 44 private final static int MAX_SPELL_BATCH_SIZE = 50; 45 46 private final TextView mTextView; 47 48 final SpellCheckerSession mSpellCheckerSession; 49 final int mCookie; 50 51 // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated 52 // SpellCheckSpan has been recycled and can be-reused. 53 // Contains null SpellCheckSpans after index mLength. 54 private int[] mIds; 55 private SpellCheckSpan[] mSpellCheckSpans; 56 // The mLength first elements of the above arrays have been initialized 57 private int mLength; 58 59 // Parsers on chunck of text, cutting text into words that will be checked 60 private SpellParser[] mSpellParsers = new SpellParser[0]; 61 62 private int mSpanSequenceCounter = 0; 63 64 public SpellChecker(TextView textView) { 65 mTextView = textView; 66 67 final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext(). 68 getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); 69 mSpellCheckerSession = textServicesManager.newSpellCheckerSession( 70 null /* not currently used by the textServicesManager */, 71 null /* null locale means use the languages defined in Settings 72 if referToSpellCheckerLanguageSettings is true */, 73 this, true /* means use the languages defined in Settings */); 74 mCookie = hashCode(); 75 76 // Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand 77 final int size = ArrayUtils.idealObjectArraySize(1); 78 mIds = new int[size]; 79 mSpellCheckSpans = new SpellCheckSpan[size]; 80 mLength = 0; 81 } 82 83 /** 84 * @return true if a spell checker session has successfully been created. Returns false if not, 85 * for instance when spell checking has been disabled in settings. 86 */ 87 private boolean isSessionActive() { 88 return mSpellCheckerSession != null; 89 } 90 91 public void closeSession() { 92 if (mSpellCheckerSession != null) { 93 mSpellCheckerSession.close(); 94 } 95 96 final int length = mSpellParsers.length; 97 for (int i = 0; i < length; i++) { 98 mSpellParsers[i].close(); 99 } 100 } 101 102 private int nextSpellCheckSpanIndex() { 103 for (int i = 0; i < mLength; i++) { 104 if (mIds[i] < 0) return i; 105 } 106 107 if (mLength == mSpellCheckSpans.length) { 108 final int newSize = mLength * 2; 109 int[] newIds = new int[newSize]; 110 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize]; 111 System.arraycopy(mIds, 0, newIds, 0, mLength); 112 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength); 113 mIds = newIds; 114 mSpellCheckSpans = newSpellCheckSpans; 115 } 116 117 mSpellCheckSpans[mLength] = new SpellCheckSpan(); 118 mLength++; 119 return mLength - 1; 120 } 121 122 private void addSpellCheckSpan(Editable editable, int start, int end) { 123 final int index = nextSpellCheckSpanIndex(); 124 editable.setSpan(mSpellCheckSpans[index], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 125 mIds[index] = mSpanSequenceCounter++; 126 } 127 128 public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) { 129 for (int i = 0; i < mLength; i++) { 130 if (mSpellCheckSpans[i] == spellCheckSpan) { 131 mSpellCheckSpans[i].setSpellCheckInProgress(false); 132 mIds[i] = -1; 133 return; 134 } 135 } 136 } 137 138 public void onSelectionChanged() { 139 spellCheck(); 140 } 141 142 public void spellCheck(int start, int end) { 143 if (!isSessionActive()) return; 144 145 final int length = mSpellParsers.length; 146 for (int i = 0; i < length; i++) { 147 final SpellParser spellParser = mSpellParsers[i]; 148 if (spellParser.isDone()) { 149 spellParser.init(start, end); 150 spellParser.parse(); 151 return; 152 } 153 } 154 155 // No available parser found in pool, create a new one 156 SpellParser[] newSpellParsers = new SpellParser[length + 1]; 157 System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length); 158 mSpellParsers = newSpellParsers; 159 160 SpellParser spellParser = new SpellParser(); 161 mSpellParsers[length] = spellParser; 162 spellParser.init(start, end); 163 spellParser.parse(); 164 } 165 166 private void spellCheck() { 167 if (mSpellCheckerSession == null) return; 168 169 Editable editable = (Editable) mTextView.getText(); 170 final int selectionStart = Selection.getSelectionStart(editable); 171 final int selectionEnd = Selection.getSelectionEnd(editable); 172 173 TextInfo[] textInfos = new TextInfo[mLength]; 174 int textInfosCount = 0; 175 176 for (int i = 0; i < mLength; i++) { 177 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; 178 if (spellCheckSpan.isSpellCheckInProgress()) continue; 179 180 final int start = editable.getSpanStart(spellCheckSpan); 181 final int end = editable.getSpanEnd(spellCheckSpan); 182 183 // Do not check this word if the user is currently editing it 184 if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) { 185 final String word = editable.subSequence(start, end).toString(); 186 spellCheckSpan.setSpellCheckInProgress(true); 187 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]); 188 } 189 } 190 191 if (textInfosCount > 0) { 192 if (textInfosCount < textInfos.length) { 193 TextInfo[] textInfosCopy = new TextInfo[textInfosCount]; 194 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount); 195 textInfos = textInfosCopy; 196 } 197 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE, 198 false /* TODO Set sequentialWords to true for initial spell check */); 199 } 200 } 201 202 @Override 203 public void onGetSuggestions(SuggestionsInfo[] results) { 204 Editable editable = (Editable) mTextView.getText(); 205 206 for (int i = 0; i < results.length; i++) { 207 SuggestionsInfo suggestionsInfo = results[i]; 208 if (suggestionsInfo.getCookie() != mCookie) continue; 209 final int sequenceNumber = suggestionsInfo.getSequence(); 210 211 for (int j = 0; j < mLength; j++) { 212 if (sequenceNumber == mIds[j]) { 213 final int attributes = suggestionsInfo.getSuggestionsAttributes(); 214 boolean isInDictionary = 215 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0); 216 boolean looksLikeTypo = 217 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); 218 219 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j]; 220 if (!isInDictionary && looksLikeTypo) { 221 createMisspelledSuggestionSpan(editable, suggestionsInfo, spellCheckSpan); 222 } 223 editable.removeSpan(spellCheckSpan); 224 break; 225 } 226 } 227 } 228 229 final int length = mSpellParsers.length; 230 for (int i = 0; i < length; i++) { 231 final SpellParser spellParser = mSpellParsers[i]; 232 if (!spellParser.isDone()) { 233 spellParser.parse(); 234 } 235 } 236 } 237 238 private void createMisspelledSuggestionSpan(Editable editable, 239 SuggestionsInfo suggestionsInfo, SpellCheckSpan spellCheckSpan) { 240 final int start = editable.getSpanStart(spellCheckSpan); 241 final int end = editable.getSpanEnd(spellCheckSpan); 242 if (start < 0 || end < 0) return; // span was removed in the meantime 243 244 // Other suggestion spans may exist on that region, with identical suggestions, filter 245 // them out to avoid duplicates. First, filter suggestion spans on that exact region. 246 SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class); 247 final int length = suggestionSpans.length; 248 for (int i = 0; i < length; i++) { 249 final int spanStart = editable.getSpanStart(suggestionSpans[i]); 250 final int spanEnd = editable.getSpanEnd(suggestionSpans[i]); 251 if (spanStart != start || spanEnd != end) { 252 suggestionSpans[i] = null; 253 } 254 } 255 256 final int suggestionsCount = suggestionsInfo.getSuggestionsCount(); 257 String[] suggestions; 258 if (suggestionsCount <= 0) { 259 // A negative suggestion count is possible 260 suggestions = ArrayUtils.emptyArray(String.class); 261 } else { 262 int numberOfSuggestions = 0; 263 suggestions = new String[suggestionsCount]; 264 265 for (int i = 0; i < suggestionsCount; i++) { 266 final String spellSuggestion = suggestionsInfo.getSuggestionAt(i); 267 if (spellSuggestion == null) break; 268 boolean suggestionFound = false; 269 270 for (int j = 0; j < length && !suggestionFound; j++) { 271 if (suggestionSpans[j] == null) break; 272 273 String[] suggests = suggestionSpans[j].getSuggestions(); 274 for (int k = 0; k < suggests.length; k++) { 275 if (spellSuggestion.equals(suggests[k])) { 276 // The suggestion is already provided by an other SuggestionSpan 277 suggestionFound = true; 278 break; 279 } 280 } 281 } 282 283 if (!suggestionFound) { 284 suggestions[numberOfSuggestions++] = spellSuggestion; 285 } 286 } 287 288 if (numberOfSuggestions != suggestionsCount) { 289 String[] newSuggestions = new String[numberOfSuggestions]; 290 System.arraycopy(suggestions, 0, newSuggestions, 0, numberOfSuggestions); 291 suggestions = newSuggestions; 292 } 293 } 294 295 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions, 296 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED); 297 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 298 299 // TODO limit to the word rectangle region 300 mTextView.invalidate(); 301 } 302 303 private class SpellParser { 304 private WordIterator mWordIterator = new WordIterator(/*TODO Locale*/); 305 private Object mRange = new Object(); 306 307 public void init(int start, int end) { 308 ((Editable) mTextView.getText()).setSpan(mRange, start, end, 309 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 310 } 311 312 public void close() { 313 ((Editable) mTextView.getText()).removeSpan(mRange); 314 } 315 316 public boolean isDone() { 317 return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0; 318 } 319 320 public void parse() { 321 Editable editable = (Editable) mTextView.getText(); 322 // Iterate over the newly added text and schedule new SpellCheckSpans 323 final int start = editable.getSpanStart(mRange); 324 final int end = editable.getSpanEnd(mRange); 325 mWordIterator.setCharSequence(editable, start, end); 326 327 // Move back to the beginning of the current word, if any 328 int wordStart = mWordIterator.preceding(start); 329 int wordEnd; 330 if (wordStart == BreakIterator.DONE) { 331 wordEnd = mWordIterator.following(start); 332 if (wordEnd != BreakIterator.DONE) { 333 wordStart = mWordIterator.getBeginning(wordEnd); 334 } 335 } else { 336 wordEnd = mWordIterator.getEnd(wordStart); 337 } 338 if (wordEnd == BreakIterator.DONE) { 339 editable.removeSpan(mRange); 340 return; 341 } 342 343 // We need to expand by one character because we want to include the spans that 344 // end/start at position start/end respectively. 345 SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1, 346 SpellCheckSpan.class); 347 SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1, 348 SuggestionSpan.class); 349 350 int nbWordsChecked = 0; 351 boolean scheduleOtherSpellCheck = false; 352 353 while (wordStart <= end) { 354 if (wordEnd >= start && wordEnd > wordStart) { 355 // A new word has been created across the interval boundaries with this edit. 356 // Previous spans (ended on start / started on end) removed, not valid anymore 357 if (wordStart < start && wordEnd > start) { 358 removeSpansAt(editable, start, spellCheckSpans); 359 removeSpansAt(editable, start, suggestionSpans); 360 } 361 362 if (wordStart < end && wordEnd > end) { 363 removeSpansAt(editable, end, spellCheckSpans); 364 removeSpansAt(editable, end, suggestionSpans); 365 } 366 367 // Do not create new boundary spans if they already exist 368 boolean createSpellCheckSpan = true; 369 if (wordEnd == start) { 370 for (int i = 0; i < spellCheckSpans.length; i++) { 371 final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]); 372 if (spanEnd == start) { 373 createSpellCheckSpan = false; 374 break; 375 } 376 } 377 } 378 379 if (wordStart == end) { 380 for (int i = 0; i < spellCheckSpans.length; i++) { 381 final int spanStart = editable.getSpanStart(spellCheckSpans[i]); 382 if (spanStart == end) { 383 createSpellCheckSpan = false; 384 break; 385 } 386 } 387 } 388 389 if (createSpellCheckSpan) { 390 if (nbWordsChecked == MAX_SPELL_BATCH_SIZE) { 391 scheduleOtherSpellCheck = true; 392 break; 393 } 394 addSpellCheckSpan(editable, wordStart, wordEnd); 395 nbWordsChecked++; 396 } 397 } 398 399 // iterate word by word 400 wordEnd = mWordIterator.following(wordEnd); 401 if (wordEnd == BreakIterator.DONE) break; 402 wordStart = mWordIterator.getBeginning(wordEnd); 403 if (wordStart == BreakIterator.DONE) { 404 break; 405 } 406 } 407 408 if (scheduleOtherSpellCheck) { 409 editable.setSpan(mRange, wordStart, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 410 } else { 411 editable.removeSpan(mRange); 412 } 413 414 spellCheck(); 415 } 416 417 private <T> void removeSpansAt(Editable editable, int offset, T[] spans) { 418 final int length = spans.length; 419 for (int i = 0; i < length; i++) { 420 final T span = spans[i]; 421 final int start = editable.getSpanStart(span); 422 if (start > offset) continue; 423 final int end = editable.getSpanEnd(span); 424 if (end < offset) continue; 425 editable.removeSpan(span); 426 } 427 } 428 } 429} 430