SpellChecker.java revision 6435a56a8c02de98befcc8cd743b2b638cffb327
1// Copyright 2011 Google Inc. All Rights Reserved. 2 3package android.widget; 4 5import android.content.Context; 6import android.text.Editable; 7import android.text.Selection; 8import android.text.Spanned; 9import android.text.style.SpellCheckSpan; 10import android.text.style.SuggestionSpan; 11import android.util.Log; 12import android.view.textservice.SpellCheckerSession; 13import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; 14import android.view.textservice.SuggestionsInfo; 15import android.view.textservice.TextInfo; 16import android.view.textservice.TextServicesManager; 17 18import com.android.internal.util.ArrayUtils; 19 20import java.util.Locale; 21 22 23/** 24 * Helper class for TextView. Bridge between the TextView and the Dictionnary service. 25 * 26 * @hide 27 */ 28public class SpellChecker implements SpellCheckerSessionListener { 29 private static final String LOG_TAG = "SpellChecker"; 30 private static final boolean DEBUG_SPELL_CHECK = false; 31 private static final int DELAY_BEFORE_SPELL_CHECK = 400; // milliseconds 32 33 private final TextView mTextView; 34 35 final SpellCheckerSession spellCheckerSession; 36 final int mCookie; 37 38 // Paired arrays for the (id, spellCheckSpan) pair. mIndex is the next available position 39 private int[] mIds; 40 private SpellCheckSpan[] mSpellCheckSpans; 41 // The actual current number of used slots in the above arrays 42 private int mLength; 43 44 private int mSpanSequenceCounter = 0; 45 private Runnable mChecker; 46 47 public SpellChecker(TextView textView) { 48 mTextView = textView; 49 50 final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext(). 51 getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); 52 spellCheckerSession = textServicesManager.newSpellCheckerSession( 53 null /* not currently used by the textServicesManager */, Locale.getDefault(), 54 this, true /* means use the languages defined in Settings */); 55 mCookie = hashCode(); 56 57 // Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand 58 final int size = ArrayUtils.idealObjectArraySize(4); 59 mIds = new int[size]; 60 mSpellCheckSpans = new SpellCheckSpan[size]; 61 mLength = 0; 62 } 63 64 public void addSpellCheckSpan(SpellCheckSpan spellCheckSpan) { 65 int length = mIds.length; 66 if (mLength >= length) { 67 final int newSize = length * 2; 68 int[] newIds = new int[newSize]; 69 SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize]; 70 System.arraycopy(mIds, 0, newIds, 0, length); 71 System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, length); 72 mIds = newIds; 73 mSpellCheckSpans = newSpellCheckSpans; 74 } 75 76 mIds[mLength] = mSpanSequenceCounter++; 77 mSpellCheckSpans[mLength] = spellCheckSpan; 78 mLength++; 79 80 if (DEBUG_SPELL_CHECK) { 81 final Editable mText = (Editable) mTextView.getText(); 82 int start = mText.getSpanStart(spellCheckSpan); 83 int end = mText.getSpanEnd(spellCheckSpan); 84 if (start >= 0 && end >= 0) { 85 Log.d(LOG_TAG, "Schedule check " + mText.subSequence(start, end)); 86 } else { 87 Log.d(LOG_TAG, "Schedule check EMPTY!"); 88 } 89 } 90 91 scheduleSpellCheck(); 92 } 93 94 public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) { 95 for (int i = 0; i < mLength; i++) { 96 if (mSpellCheckSpans[i] == spellCheckSpan) { 97 removeAtIndex(i); 98 return; 99 } 100 } 101 } 102 103 private void removeAtIndex(int i) { 104 System.arraycopy(mIds, i + 1, mIds, i, mLength - i - 1); 105 System.arraycopy(mSpellCheckSpans, i + 1, mSpellCheckSpans, i, mLength - i - 1); 106 mLength--; 107 } 108 109 public void onSelectionChanged() { 110 scheduleSpellCheck(); 111 } 112 113 private void scheduleSpellCheck() { 114 if (mLength == 0) return; 115 if (mChecker != null) { 116 mTextView.removeCallbacks(mChecker); 117 } 118 if (mChecker == null) { 119 mChecker = new Runnable() { 120 public void run() { 121 spellCheck(); 122 } 123 }; 124 } 125 mTextView.postDelayed(mChecker, DELAY_BEFORE_SPELL_CHECK); 126 } 127 128 private void spellCheck() { 129 final Editable editable = (Editable) mTextView.getText(); 130 final int selectionStart = Selection.getSelectionStart(editable); 131 final int selectionEnd = Selection.getSelectionEnd(editable); 132 133 TextInfo[] textInfos = new TextInfo[mLength]; 134 int textInfosCount = 0; 135 136 for (int i = 0; i < mLength; i++) { 137 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; 138 139 if (spellCheckSpan.isSpellCheckInProgress()) continue; 140 141 final int start = editable.getSpanStart(spellCheckSpan); 142 final int end = editable.getSpanEnd(spellCheckSpan); 143 144 // Do not check this word if the user is currently editing it 145 if (start >= 0 && end >= 0 && (selectionEnd < start || selectionStart > end)) { 146 final String word = editable.subSequence(start, end).toString(); 147 spellCheckSpan.setSpellCheckInProgress(); 148 textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]); 149 } 150 } 151 152 if (textInfosCount > 0) { 153 if (textInfosCount < mLength) { 154 TextInfo[] textInfosCopy = new TextInfo[textInfosCount]; 155 System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount); 156 textInfos = textInfosCopy; 157 } 158 spellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE, 159 false /* TODO Set sequentialWords to true for initial spell check */); 160 } 161 } 162 163 @Override 164 public void onGetSuggestions(SuggestionsInfo[] results) { 165 final Editable editable = (Editable) mTextView.getText(); 166 for (int i = 0; i < results.length; i++) { 167 SuggestionsInfo suggestionsInfo = results[i]; 168 if (suggestionsInfo.getCookie() != mCookie) continue; 169 170 final int sequenceNumber = suggestionsInfo.getSequence(); 171 // Starting from the end, to limit the number of array copy while removing 172 for (int j = mLength - 1; j >= 0; j--) { 173 if (sequenceNumber == mIds[j]) { 174 SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j]; 175 final int attributes = suggestionsInfo.getSuggestionsAttributes(); 176 boolean isInDictionary = 177 ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0); 178 boolean looksLikeTypo = 179 ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); 180 181 if (DEBUG_SPELL_CHECK) { 182 final int start = editable.getSpanStart(spellCheckSpan); 183 final int end = editable.getSpanEnd(spellCheckSpan); 184 Log.d(LOG_TAG, "Result sequence=" + suggestionsInfo.getSequence() + " " + 185 editable.subSequence(start, end) + 186 "\t" + (isInDictionary?"IN_DICT" : "NOT_DICT") + 187 "\t" + (looksLikeTypo?"TYPO" : "NOT_TYPO")); 188 } 189 190 if (!isInDictionary && looksLikeTypo) { 191 String[] suggestions = getSuggestions(suggestionsInfo); 192 if (suggestions.length > 0) { 193 SuggestionSpan suggestionSpan = new SuggestionSpan( 194 mTextView.getContext(), suggestions, 195 SuggestionSpan.FLAG_EASY_CORRECT | 196 SuggestionSpan.FLAG_MISSPELLED); 197 final int start = editable.getSpanStart(spellCheckSpan); 198 final int end = editable.getSpanEnd(spellCheckSpan); 199 editable.setSpan(suggestionSpan, start, end, 200 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 201 // TODO limit to the word rectangle region 202 mTextView.invalidate(); 203 204 if (DEBUG_SPELL_CHECK) { 205 String suggestionsString = ""; 206 for (String s : suggestions) { suggestionsString += s + "|"; } 207 Log.d(LOG_TAG, " Suggestions for " + sequenceNumber + " " + 208 editable.subSequence(start, end)+ " " + suggestionsString); 209 } 210 } 211 } 212 editable.removeSpan(spellCheckSpan); 213 } 214 } 215 } 216 } 217 218 private static String[] getSuggestions(SuggestionsInfo suggestionsInfo) { 219 final int len = Math.max(0, suggestionsInfo.getSuggestionsCount()); 220 String[] suggestions = new String[len]; 221 for (int j = 0; j < len; ++j) { 222 suggestions[j] = suggestionsInfo.getSuggestionAt(j); 223 } 224 return suggestions; 225 } 226} 227