SpellChecker.java revision 6e405f84b8d0c7c0f939bc8e9bec17e65e704a2d
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.util.Log; 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.util.Locale; 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 private static final String LOG_TAG = "SpellChecker"; 44 private static final boolean DEBUG_SPELL_CHECK = false; 45 private static final int DELAY_BEFORE_SPELL_CHECK = 400; // milliseconds 46 47 private final TextView mTextView; 48 49 final SpellCheckerSession mSpellCheckerSession; 50 final int mCookie; 51 52 // Paired arrays for the (id, spellCheckSpan) pair. mIndex is the next available position 53 private int[] mIds; 54 private SpellCheckSpan[] mSpellCheckSpans; 55 // The actual current number of used slots in the above arrays 56 private int mLength; 57 58 private int mSpanSequenceCounter = 0; 59 private Runnable mChecker; 60 61 public SpellChecker(TextView textView) { 62 mTextView = textView; 63 64 final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext(). 65 getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); 66 mSpellCheckerSession = textServicesManager.newSpellCheckerSession( 67 null /* not currently used by the textServicesManager */, Locale.getDefault(), 68 this, true /* means use the languages defined in Settings */); 69 mCookie = hashCode(); 70 71 // Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand 72 final int size = ArrayUtils.idealObjectArraySize(4); 73 mIds = new int[size]; 74 mSpellCheckSpans = new SpellCheckSpan[size]; 75 mLength = 0; 76 } 77 78 /** 79 * @return true if a spell checker session has successfully been created. Returns false if not, 80 * for instance when spell checking has been disabled in settings. 81 */ 82 public boolean isSessionActive() { 83 return mSpellCheckerSession != null; 84 } 85 86 public void closeSession() { 87 if (mSpellCheckerSession != null) { 88 mSpellCheckerSession.close(); 89 } 90 } 91 92 public void addSpellCheckSpan(SpellCheckSpan spellCheckSpan) { 93 int length = mIds.length; 94 if (mLength >= length) { 95 final int newSize = length * 2; 96 int[] newIds = new int[newSize]; 97 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize]; 98 System.arraycopy(mIds, 0, newIds, 0, length); 99 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, length); 100 mIds = newIds; 101 mSpellCheckSpans = newSpellCheckSpans; 102 } 103 104 mIds[mLength] = mSpanSequenceCounter++; 105 mSpellCheckSpans[mLength] = spellCheckSpan; 106 mLength++; 107 108 if (DEBUG_SPELL_CHECK) { 109 final Editable mText = (Editable) mTextView.getText(); 110 int start = mText.getSpanStart(spellCheckSpan); 111 int end = mText.getSpanEnd(spellCheckSpan); 112 if (start >= 0 && end >= 0) { 113 Log.d(LOG_TAG, "Schedule check " + mText.subSequence(start, end)); 114 } else { 115 Log.d(LOG_TAG, "Schedule check EMPTY!"); 116 } 117 } 118 119 scheduleSpellCheck(); 120 } 121 122 public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) { 123 for (int i = 0; i < mLength; i++) { 124 if (mSpellCheckSpans[i] == spellCheckSpan) { 125 removeAtIndex(i); 126 return; 127 } 128 } 129 } 130 131 private void removeAtIndex(int i) { 132 System.arraycopy(mIds, i + 1, mIds, i, mLength - i - 1); 133 System.arraycopy(mSpellCheckSpans, i + 1, mSpellCheckSpans, i, mLength - i - 1); 134 mLength--; 135 } 136 137 public void onSelectionChanged() { 138 scheduleSpellCheck(); 139 } 140 141 private void scheduleSpellCheck() { 142 if (mLength == 0) return; 143 if (mSpellCheckerSession == null) return; 144 145 if (mChecker != null) { 146 mTextView.removeCallbacks(mChecker); 147 } 148 if (mChecker == null) { 149 mChecker = new Runnable() { 150 public void run() { 151 spellCheck(); 152 } 153 }; 154 } 155 mTextView.postDelayed(mChecker, DELAY_BEFORE_SPELL_CHECK); 156 } 157 158 private void spellCheck() { 159 final Editable editable = (Editable) mTextView.getText(); 160 final int selectionStart = Selection.getSelectionStart(editable); 161 final int selectionEnd = Selection.getSelectionEnd(editable); 162 163 TextInfo[] textInfos = new TextInfo[mLength]; 164 int textInfosCount = 0; 165 166 for (int i = 0; i < mLength; i++) { 167 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; 168 169 if (spellCheckSpan.isSpellCheckInProgress()) continue; 170 171 final int start = editable.getSpanStart(spellCheckSpan); 172 final int end = editable.getSpanEnd(spellCheckSpan); 173 174 // Do not check this word if the user is currently editing it 175 if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) { 176 final String word = editable.subSequence(start, end).toString(); 177 spellCheckSpan.setSpellCheckInProgress(); 178 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]); 179 } 180 } 181 182 if (textInfosCount > 0) { 183 if (textInfosCount < mLength) { 184 TextInfo[] textInfosCopy = new TextInfo[textInfosCount]; 185 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount); 186 textInfos = textInfosCopy; 187 } 188 mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE, 189 false /* TODO Set sequentialWords to true for initial spell check */); 190 } 191 } 192 193 @Override 194 public void onGetSuggestions(SuggestionsInfo[] results) { 195 final Editable editable = (Editable) mTextView.getText(); 196 for (int i = 0; i < results.length; i++) { 197 SuggestionsInfo suggestionsInfo = results[i]; 198 if (suggestionsInfo.getCookie() != mCookie) continue; 199 200 final int sequenceNumber = suggestionsInfo.getSequence(); 201 // Starting from the end, to limit the number of array copy while removing 202 for (int j = mLength - 1; j >= 0; j--) { 203 if (sequenceNumber == mIds[j]) { 204 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j]; 205 final int attributes = suggestionsInfo.getSuggestionsAttributes(); 206 boolean isInDictionary = 207 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0); 208 boolean looksLikeTypo = 209 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); 210 211 if (DEBUG_SPELL_CHECK) { 212 final int start = editable.getSpanStart(spellCheckSpan); 213 final int end = editable.getSpanEnd(spellCheckSpan); 214 Log.d(LOG_TAG, "Result sequence=" + suggestionsInfo.getSequence() + " " + 215 editable.subSequence(start, end) + 216 "\t" + (isInDictionary?"IN_DICT" : "NOT_DICT") + 217 "\t" + (looksLikeTypo?"TYPO" : "NOT_TYPO")); 218 } 219 220 if (!isInDictionary && looksLikeTypo) { 221 String[] suggestions = getSuggestions(suggestionsInfo); 222 if (suggestions.length > 0) { 223 SuggestionSpan suggestionSpan = new SuggestionSpan( 224 mTextView.getContext(), suggestions, 225 SuggestionSpan.FLAG_EASY_CORRECT | 226 SuggestionSpan.FLAG_MISSPELLED); 227 final int start = editable.getSpanStart(spellCheckSpan); 228 final int end = editable.getSpanEnd(spellCheckSpan); 229 editable.setSpan(suggestionSpan, start, end, 230 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 231 // TODO limit to the word rectangle region 232 mTextView.invalidate(); 233 234 if (DEBUG_SPELL_CHECK) { 235 String suggestionsString = ""; 236 for (String s : suggestions) { suggestionsString += s + "|"; } 237 Log.d(LOG_TAG, " Suggestions for " + sequenceNumber + " " + 238 editable.subSequence(start, end)+ " " + suggestionsString); 239 } 240 } 241 } 242 editable.removeSpan(spellCheckSpan); 243 } 244 } 245 } 246 } 247 248 private static String[] getSuggestions(SuggestionsInfo suggestionsInfo) { 249 final int len = Math.max(0, suggestionsInfo.getSuggestionsCount()); 250 String[] suggestions = new String[len]; 251 for (int j = 0; j < len; ++j) { 252 suggestions[j] = suggestionsInfo.getSuggestionAt(j); 253 } 254 return suggestions; 255 } 256} 257