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