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