AndroidSpellCheckerService.java revision 6b166a193398554694cb680f704c2ffc23d03a0e
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * 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, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.latin.spellcheck;
18
19import android.content.Intent;
20import android.content.res.Resources;
21import android.service.textservice.SpellCheckerService;
22import android.service.textservice.SpellCheckerService.Session;
23import android.util.Log;
24import android.view.textservice.SuggestionsInfo;
25import android.view.textservice.TextInfo;
26import android.text.TextUtils;
27
28import com.android.inputmethod.compat.ArraysCompatUtils;
29import com.android.inputmethod.keyboard.Key;
30import com.android.inputmethod.keyboard.ProximityInfo;
31import com.android.inputmethod.latin.Dictionary;
32import com.android.inputmethod.latin.Dictionary.DataType;
33import com.android.inputmethod.latin.Dictionary.WordCallback;
34import com.android.inputmethod.latin.DictionaryCollection;
35import com.android.inputmethod.latin.DictionaryFactory;
36import com.android.inputmethod.latin.UserDictionary;
37import com.android.inputmethod.latin.Utils;
38import com.android.inputmethod.latin.WordComposer;
39
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Collections;
43import java.util.Locale;
44import java.util.Map;
45import java.util.TreeMap;
46
47/**
48 * Service for spell checking, using LatinIME's dictionaries and mechanisms.
49 */
50public class AndroidSpellCheckerService extends SpellCheckerService {
51    private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
52    private static final boolean DBG = false;
53    private static final int POOL_SIZE = 2;
54
55    private final static String[] EMPTY_STRING_ARRAY = new String[0];
56    private final static SuggestionsInfo EMPTY_SUGGESTIONS_INFO =
57            new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
58    private Map<String, DictionaryPool> mDictionaryPools =
59            Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
60    private Map<String, Dictionary> mUserDictionaries =
61            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
62
63    @Override
64    public Session createSession() {
65        return new AndroidSpellCheckerSession();
66    }
67
68    private static class SuggestionsGatherer implements WordCallback {
69        private final int DEFAULT_SUGGESTION_LENGTH = 16;
70        private final ArrayList<CharSequence> mSuggestions;
71        private final int[] mScores;
72        private final int mMaxLength;
73        private int mLength = 0;
74        private boolean mSeenSuggestions = false;
75
76        SuggestionsGatherer(final int maxLength) {
77            mMaxLength = maxLength;
78            mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
79            mScores = new int[mMaxLength];
80        }
81
82        @Override
83        synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
84                int dicTypeId, DataType dataType) {
85            final int positionIndex = ArraysCompatUtils.binarySearch(mScores, 0, mLength, score);
86            // binarySearch returns the index if the element exists, and -<insertion index> - 1
87            // if it doesn't. See documentation for binarySearch.
88            final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
89
90            mSeenSuggestions = true;
91            if (mLength < mMaxLength) {
92                final int copyLen = mLength - insertIndex;
93                ++mLength;
94                System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen);
95                mSuggestions.add(insertIndex, new String(word, wordOffset, wordLength));
96            } else {
97                if (insertIndex == 0) return true;
98                System.arraycopy(mScores, 1, mScores, 0, insertIndex);
99                mSuggestions.add(insertIndex, new String(word, wordOffset, wordLength));
100                mSuggestions.remove(0);
101            }
102            mScores[insertIndex] = score;
103
104            return true;
105        }
106
107        public String[] getGatheredSuggestions() {
108            if (!mSeenSuggestions) return null;
109            if (0 == mLength) return EMPTY_STRING_ARRAY;
110
111            if (DBG) {
112                if (mLength != mSuggestions.size()) {
113                    Log.e(TAG, "Suggestion size is not the same as stored mLength");
114                }
115            }
116            Collections.reverse(mSuggestions);
117            Utils.removeDupes(mSuggestions);
118            // This returns a String[], while toArray() returns an Object[] which cannot be cast
119            // into a String[].
120            return mSuggestions.toArray(EMPTY_STRING_ARRAY);
121        }
122    }
123
124    @Override
125    public boolean onUnbind(final Intent intent) {
126        final Map<String, DictionaryPool> oldPools = mDictionaryPools;
127        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
128        final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
129        mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
130        for (DictionaryPool pool : oldPools.values()) {
131            pool.close();
132        }
133        for (Dictionary dict : oldUserDictionaries.values()) {
134            dict.close();
135        }
136        return false;
137    }
138
139    private DictionaryPool getDictionaryPool(final String locale) {
140        DictionaryPool pool = mDictionaryPools.get(locale);
141        if (null == pool) {
142            final Locale localeObject = Utils.constructLocaleFromString(locale);
143            pool = new DictionaryPool(POOL_SIZE, this, localeObject);
144            mDictionaryPools.put(locale, pool);
145        }
146        return pool;
147    }
148
149    public DictAndProximity createDictAndProximity(final Locale locale) {
150        final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo();
151        final Resources resources = getResources();
152        final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources);
153        final DictionaryCollection dictionaryCollection =
154                DictionaryFactory.createDictionaryFromManager(this, locale, fallbackResourceId);
155        final String localeStr = locale.toString();
156        Dictionary userDict = mUserDictionaries.get(localeStr);
157        if (null == userDict) {
158            userDict = new UserDictionary(this, localeStr);
159            mUserDictionaries.put(localeStr, userDict);
160        }
161        dictionaryCollection.addDictionary(userDict);
162        return new DictAndProximity(dictionaryCollection, proximityInfo);
163    }
164
165    private class AndroidSpellCheckerSession extends Session {
166        // Immutable, but need the locale which is not available in the constructor yet
167        DictionaryPool mDictionaryPool;
168        // Likewise
169        Locale mLocale;
170
171        @Override
172        public void onCreate() {
173            final String localeString = getLocale();
174            mDictionaryPool = getDictionaryPool(localeString);
175            mLocale = Utils.constructLocaleFromString(localeString);
176        }
177
178        // Note : this must be reentrant
179        /**
180         * Gets a list of suggestions for a specific string. This returns a list of possible
181         * corrections for the text passed as an argument. It may split or group words, and
182         * even perform grammatical analysis.
183         */
184        @Override
185        public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
186                final int suggestionsLimit) {
187            final String text = textInfo.getText();
188
189            if (TextUtils.isEmpty(text)) return EMPTY_SUGGESTIONS_INFO;
190
191            final SuggestionsGatherer suggestionsGatherer =
192                    new SuggestionsGatherer(suggestionsLimit);
193            final WordComposer composer = new WordComposer();
194            final int length = text.length();
195            for (int i = 0; i < length; ++i) {
196                final int character = text.codePointAt(i);
197                final int proximityIndex = SpellCheckerProximityInfo.getIndexOf(character);
198                final int[] proximities;
199                if (-1 == proximityIndex) {
200                    proximities = new int[] { character };
201                } else {
202                    proximities = Arrays.copyOfRange(SpellCheckerProximityInfo.PROXIMITY,
203                            proximityIndex, proximityIndex + SpellCheckerProximityInfo.ROW_SIZE);
204                }
205                composer.add(character, proximities,
206                        WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
207            }
208
209            boolean isInDict = true;
210            try {
211                final DictAndProximity dictInfo = mDictionaryPool.take();
212                dictInfo.mDictionary.getWords(composer, suggestionsGatherer,
213                        dictInfo.mProximityInfo);
214                isInDict = dictInfo.mDictionary.isValidWord(text);
215                if (!isInDict && Character.isUpperCase(text.codePointAt(0))) {
216                    // If the first char is not uppercase, then the word is either all lower case,
217                    // in which case we already tested it, or mixed case, in which case we don't
218                    // want to test a lower-case version of it. Hence the test above.
219                    // Also note that by isEmpty() test at the top of the method codePointAt(0) is
220                    // guaranteed to be there.
221                    final int len = text.codePointCount(0, text.length());
222                    int capsCount = 1;
223                    for (int i = 1; i < len; ++i) {
224                        if (1 != capsCount && i != capsCount) break;
225                        if (Character.isUpperCase(text.codePointAt(i))) ++capsCount;
226                    }
227                    // We know the first char is upper case. So we want to test if either everything
228                    // else is lower case, or if everything else is upper case. If the string is
229                    // exactly one char long, then we will arrive here with capsCount 0, and this is
230                    // correct, too.
231                    if (1 == capsCount || len == capsCount) {
232                        isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
233                    }
234                }
235                if (!mDictionaryPool.offer(dictInfo)) {
236                    Log.e(TAG, "Can't re-insert a dictionary into its pool");
237                }
238            } catch (InterruptedException e) {
239                // I don't think this can happen.
240                return EMPTY_SUGGESTIONS_INFO;
241            }
242
243            final String[] suggestions = suggestionsGatherer.getGatheredSuggestions();
244
245            final int flags =
246                    (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY : 0)
247                            | (null != suggestions
248                                    ? SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO : 0);
249            return new SuggestionsInfo(flags, suggestions);
250        }
251    }
252}
253