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