SpellChecker.java revision a80599f5be394edd9f3918ba03c490850a1d9e7f
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    public void addSpellCheckSpan(SpellCheckSpan spellCheckSpan) {
79        int length = mIds.length;
80        if (mLength >= length) {
81            final int newSize = length * 2;
82            int[] newIds = new int[newSize];
83            SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize];
84            System.arraycopy(mIds, 0, newIds, 0, length);
85            System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, length);
86            mIds = newIds;
87            mSpellCheckSpans = newSpellCheckSpans;
88        }
89
90        mIds[mLength] = mSpanSequenceCounter++;
91        mSpellCheckSpans[mLength] = spellCheckSpan;
92        mLength++;
93
94        if (DEBUG_SPELL_CHECK) {
95            final Editable mText = (Editable) mTextView.getText();
96            int start = mText.getSpanStart(spellCheckSpan);
97            int end = mText.getSpanEnd(spellCheckSpan);
98            if (start >= 0 && end >= 0) {
99                Log.d(LOG_TAG, "Schedule check " + mText.subSequence(start, end));
100            } else {
101                Log.d(LOG_TAG, "Schedule check   EMPTY!");
102            }
103        }
104
105        scheduleSpellCheck();
106    }
107
108    public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) {
109        for (int i = 0; i < mLength; i++) {
110            if (mSpellCheckSpans[i] == spellCheckSpan) {
111                removeAtIndex(i);
112                return;
113            }
114        }
115    }
116
117    private void removeAtIndex(int i) {
118        System.arraycopy(mIds, i + 1, mIds, i, mLength - i - 1);
119        System.arraycopy(mSpellCheckSpans, i + 1, mSpellCheckSpans, i, mLength - i - 1);
120        mLength--;
121    }
122
123    public void onSelectionChanged() {
124        scheduleSpellCheck();
125    }
126
127    private void scheduleSpellCheck() {
128        if (mLength == 0) return;
129        if (mSpellCheckerSession == null) return;
130
131        if (mChecker != null) {
132            mTextView.removeCallbacks(mChecker);
133        }
134        if (mChecker == null) {
135            mChecker = new Runnable() {
136                public void run() {
137                  spellCheck();
138                }
139            };
140        }
141        mTextView.postDelayed(mChecker, DELAY_BEFORE_SPELL_CHECK);
142    }
143
144    private void spellCheck() {
145        final Editable editable = (Editable) mTextView.getText();
146        final int selectionStart = Selection.getSelectionStart(editable);
147        final int selectionEnd = Selection.getSelectionEnd(editable);
148
149        TextInfo[] textInfos = new TextInfo[mLength];
150        int textInfosCount = 0;
151
152        for (int i = 0; i < mLength; i++) {
153            SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
154
155            if (spellCheckSpan.isSpellCheckInProgress()) continue;
156
157            final int start = editable.getSpanStart(spellCheckSpan);
158            final int end = editable.getSpanEnd(spellCheckSpan);
159
160            // Do not check this word if the user is currently editing it
161            if (start >= 0 && end > start && (selectionEnd < start || selectionStart > end)) {
162                final String word = editable.subSequence(start, end).toString();
163                spellCheckSpan.setSpellCheckInProgress();
164                textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]);
165            }
166        }
167
168        if (textInfosCount > 0) {
169            if (textInfosCount < mLength) {
170                TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
171                System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
172                textInfos = textInfosCopy;
173            }
174            mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
175                    false /* TODO Set sequentialWords to true for initial spell check */);
176        }
177    }
178
179    @Override
180    public void onGetSuggestions(SuggestionsInfo[] results) {
181        final Editable editable = (Editable) mTextView.getText();
182        for (int i = 0; i < results.length; i++) {
183            SuggestionsInfo suggestionsInfo = results[i];
184            if (suggestionsInfo.getCookie() != mCookie) continue;
185
186            final int sequenceNumber = suggestionsInfo.getSequence();
187            // Starting from the end, to limit the number of array copy while removing
188            for (int j = mLength - 1; j >= 0; j--) {
189                if (sequenceNumber == mIds[j]) {
190                    SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j];
191                    final int attributes = suggestionsInfo.getSuggestionsAttributes();
192                    boolean isInDictionary =
193                            ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
194                    boolean looksLikeTypo =
195                            ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
196
197                    if (DEBUG_SPELL_CHECK) {
198                        final int start = editable.getSpanStart(spellCheckSpan);
199                        final int end = editable.getSpanEnd(spellCheckSpan);
200                        Log.d(LOG_TAG, "Result sequence=" + suggestionsInfo.getSequence() + " " +
201                                editable.subSequence(start, end) +
202                                "\t" + (isInDictionary?"IN_DICT" : "NOT_DICT") +
203                                "\t" + (looksLikeTypo?"TYPO" : "NOT_TYPO"));
204                    }
205
206                    if (!isInDictionary && looksLikeTypo) {
207                        String[] suggestions = getSuggestions(suggestionsInfo);
208                        if (suggestions.length > 0) {
209                            SuggestionSpan suggestionSpan = new SuggestionSpan(
210                                    mTextView.getContext(), suggestions,
211                                    SuggestionSpan.FLAG_EASY_CORRECT |
212                                    SuggestionSpan.FLAG_MISSPELLED);
213                            final int start = editable.getSpanStart(spellCheckSpan);
214                            final int end = editable.getSpanEnd(spellCheckSpan);
215                            editable.setSpan(suggestionSpan, start, end,
216                                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
217                            // TODO limit to the word rectangle region
218                            mTextView.invalidate();
219
220                            if (DEBUG_SPELL_CHECK) {
221                                String suggestionsString = "";
222                                for (String s : suggestions) { suggestionsString += s + "|"; }
223                                Log.d(LOG_TAG, "  Suggestions for " + sequenceNumber + " " +
224                                    editable.subSequence(start, end)+ "  " + suggestionsString);
225                            }
226                        }
227                    }
228                    editable.removeSpan(spellCheckSpan);
229                }
230            }
231        }
232    }
233
234    private static String[] getSuggestions(SuggestionsInfo suggestionsInfo) {
235        final int len = Math.max(0, suggestionsInfo.getSuggestionsCount());
236        String[] suggestions = new String[len];
237        for (int j = 0; j < len; ++j) {
238            suggestions[j] = suggestionsInfo.getSuggestionAt(j);
239        }
240        return suggestions;
241    }
242}
243