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