blob: 5e3b956e83a3af6bbe43def8368be84420268d62 [file] [log] [blame]
Gilles Debunne6435a562011-08-04 21:22:30 -07001// 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}