SpellChecker.java revision e1fc4f6c3c7d573f013b707ee962d58f9fb636dd
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.style.SpellCheckSpan; 24import android.text.style.SuggestionSpan; 25import android.view.textservice.SpellCheckerSession; 26import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; 27import android.view.textservice.SuggestionsInfo; 28import android.view.textservice.TextInfo; 29import android.view.textservice.TextServicesManager; 30 31import com.android.internal.util.ArrayUtils; 32 33 34/** 35 * Helper class for TextView. Bridge between the TextView and the Dictionnary service. 36 * 37 * @hide 38 */ 39public class SpellChecker implements SpellCheckerSessionListener { 40 41 private final TextView mTextView; 42 43 final SpellCheckerSession mSpellCheckerSession; 44 final int mCookie; 45 46 // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated 47 // SpellCheckSpan has been recycled and can be-reused. 48 // May contain null SpellCheckSpans after a given index. 49 private int[] mIds; 50 private SpellCheckSpan[] mSpellCheckSpans; 51 // The mLength first elements of the above arrays have been initialized 52 private int mLength; 53 54 private int mSpanSequenceCounter = 0; 55 56 public SpellChecker(TextView textView) { 57 mTextView = textView; 58 59 final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext(). 60 getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); 61 mSpellCheckerSession = textServicesManager.newSpellCheckerSession( 62 null /* not currently used by the textServicesManager */, 63 null /* null locale means use the languages defined in Settings 64 if referToSpellCheckerLanguageSettings is true */, 65 this, true /* means use the languages defined in Settings */); 66 mCookie = hashCode(); 67 68 // Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand 69 final int size = ArrayUtils.idealObjectArraySize(1); 70 mIds = new int[size]; 71 mSpellCheckSpans = new SpellCheckSpan[size]; 72 mLength = 0; 73 } 74 75 /** 76 * @return true if a spell checker session has successfully been created. Returns false if not, 77 * for instance when spell checking has been disabled in settings. 78 */ 79 public boolean isSessionActive() { 80 return mSpellCheckerSession != null; 81 } 82 83 public void closeSession() { 84 if (mSpellCheckerSession != null) { 85 mSpellCheckerSession.close(); 86 } 87 } 88 89 private int nextSpellCheckSpanIndex() { 90 for (int i = 0; i < mLength; i++) { 91 if (mIds[i] < 0) return i; 92 } 93 94 if (mLength == mSpellCheckSpans.length) { 95 final int newSize = mLength * 2; 96 int[] newIds = new int[newSize]; 97 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize]; 98 System.arraycopy(mIds, 0, newIds, 0, mLength); 99 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, mLength); 100 mIds = newIds; 101 mSpellCheckSpans = newSpellCheckSpans; 102 } 103 104 mSpellCheckSpans[mLength] = new SpellCheckSpan(); 105 mLength++; 106 return mLength - 1; 107 } 108 109 public void addSpellCheckSpan(int wordStart, int wordEnd) { 110 final int index = nextSpellCheckSpanIndex(); 111 ((Editable) mTextView.getText()).setSpan(mSpellCheckSpans[index], wordStart, wordEnd, 112 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 113 mIds[index] = mSpanSequenceCounter++; 114 } 115 116 public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) { 117 for (int i = 0; i < mLength; i++) { 118 if (mSpellCheckSpans[i] == spellCheckSpan) { 119 mSpellCheckSpans[i].setSpellCheckInProgress(false); 120 mIds[i] = -1; 121 return; 122 } 123 } 124 } 125 126 public void onSelectionChanged() { 127 spellCheck(); 128 } 129 130 public void spellCheck() { 131 if (mSpellCheckerSession == null) return; 132 133 final Editable editable = (Editable) mTextView.getText(); 134 final int selectionStart = Selection.getSelectionStart(editable); 135 final int selectionEnd = Selection.getSelectionEnd(editable); 136 137 TextInfo[] textInfos = new TextInfo[mLength]; 138 int textInfosCount = 0; 139 140 for (int i = 0; i < mLength; i++) { 141 final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; 142 if (spellCheckSpan.isSpellCheckInProgress()) continue; 143 144 final int start = editable.getSpanStart(spellCheckSpan); 145 final int end = editable.getSpanEnd(spellCheckSpan); 146 147 // Do not check this word if the user is currently editing it 148 if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) { 149 final String word = editable.subSequence(start, end).toString(); 150 spellCheckSpan.setSpellCheckInProgress(true); 151 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]); 152 } 153 } 154 155 if (textInfosCount > 0) { 156 if (textInfosCount < mLength) { 157 TextInfo[] textInfosCopy = new TextInfo[textInfosCount]; 158 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount); 159 textInfos = textInfosCopy; 160 } 161 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE, 162 false /* TODO Set sequentialWords to true for initial spell check */); 163 } 164 } 165 166 @Override 167 public void onGetSuggestions(SuggestionsInfo[] results) { 168 final Editable editable = (Editable) mTextView.getText(); 169 for (int i = 0; i < results.length; i++) { 170 SuggestionsInfo suggestionsInfo = results[i]; 171 if (suggestionsInfo.getCookie() != mCookie) continue; 172 final int sequenceNumber = suggestionsInfo.getSequence(); 173 174 for (int j = 0; j < mLength; j++) { 175 if (sequenceNumber == mIds[j]) { 176 final int attributes = suggestionsInfo.getSuggestionsAttributes(); 177 boolean isInDictionary = 178 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0); 179 boolean looksLikeTypo = 180 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); 181 182 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j]; 183 if (!isInDictionary && looksLikeTypo) { 184 createMisspelledSuggestionSpan(editable, suggestionsInfo, spellCheckSpan); 185 } 186 editable.removeSpan(spellCheckSpan); 187 break; 188 } 189 } 190 } 191 } 192 193 private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo, 194 SpellCheckSpan spellCheckSpan) { 195 final int start = editable.getSpanStart(spellCheckSpan); 196 final int end = editable.getSpanEnd(spellCheckSpan); 197 198 // Other suggestion spans may exist on that region, with identical suggestions, filter 199 // them out to avoid duplicates. First, filter suggestion spans on that exact region. 200 SuggestionSpan[] suggestionSpans = editable.getSpans(start, end, SuggestionSpan.class); 201 final int length = suggestionSpans.length; 202 for (int i = 0; i < length; i++) { 203 final int spanStart = editable.getSpanStart(suggestionSpans[i]); 204 final int spanEnd = editable.getSpanEnd(suggestionSpans[i]); 205 if (spanStart != start || spanEnd != end) { 206 suggestionSpans[i] = null; 207 break; 208 } 209 } 210 211 final int suggestionsCount = suggestionsInfo.getSuggestionsCount(); 212 String[] suggestions; 213 if (suggestionsCount <= 0) { 214 // A negative suggestion count is possible 215 suggestions = ArrayUtils.emptyArray(String.class); 216 } else { 217 int numberOfSuggestions = 0; 218 suggestions = new String[suggestionsCount]; 219 220 for (int i = 0; i < suggestionsCount; i++) { 221 final String spellSuggestion = suggestionsInfo.getSuggestionAt(i); 222 if (spellSuggestion == null) break; 223 boolean suggestionFound = false; 224 225 for (int j = 0; j < length && !suggestionFound; j++) { 226 if (suggestionSpans[j] == null) break; 227 228 String[] suggests = suggestionSpans[j].getSuggestions(); 229 for (int k = 0; k < suggests.length; k++) { 230 if (spellSuggestion.equals(suggests[k])) { 231 // The suggestion is already provided by an other SuggestionSpan 232 suggestionFound = true; 233 break; 234 } 235 } 236 } 237 238 if (!suggestionFound) { 239 suggestions[numberOfSuggestions++] = spellSuggestion; 240 } 241 } 242 243 if (numberOfSuggestions != suggestionsCount) { 244 String[] newSuggestions = new String[numberOfSuggestions]; 245 System.arraycopy(suggestions, 0, newSuggestions, 0, numberOfSuggestions); 246 suggestions = newSuggestions; 247 } 248 } 249 250 SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions, 251 SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED); 252 editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 253 254 // TODO limit to the word rectangle region 255 mTextView.invalidate(); 256 } 257} 258