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