aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
blob: a6a5b6dd66a2de8fc0ff3955e07c57ef46221908 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.android.inputmethod.latin.spellcheck;

import android.content.Intent;
import android.content.res.Resources;
import android.service.textservice.SpellCheckerService;
import android.service.textservice.SpellCheckerService.Session;
import android.util.Log;
import android.view.textservice.SuggestionsInfo;
import android.view.textservice.TextInfo;
import android.text.TextUtils;

import com.android.inputmethod.compat.ArraysCompatUtils;
import com.android.inputmethod.keyboard.Key;
import com.android.inputmethod.keyboard.ProximityInfo;
import com.android.inputmethod.latin.Dictionary;
import com.android.inputmethod.latin.Dictionary.DataType;
import com.android.inputmethod.latin.Dictionary.WordCallback;
import com.android.inputmethod.latin.DictionaryCollection;
import com.android.inputmethod.latin.DictionaryFactory;
import com.android.inputmethod.latin.LocaleUtils;
import com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
import com.android.inputmethod.latin.UserDictionary;
import com.android.inputmethod.latin.Utils;
import com.android.inputmethod.latin.WordComposer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

/**
 * Service for spell checking, using LatinIME's dictionaries and mechanisms.
 */
public class AndroidSpellCheckerService extends SpellCheckerService {
    private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
    private static final boolean DBG = false;
    private static final int POOL_SIZE = 2;

    private final static String[] EMPTY_STRING_ARRAY = new String[0];
    private final static SuggestionsInfo EMPTY_SUGGESTIONS_INFO =
            new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
    private Map<String, DictionaryPool> mDictionaryPools =
            Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
    private Map<String, Dictionary> mUserDictionaries =
            Collections.synchronizedMap(new TreeMap<String, Dictionary>());

    @Override
    public Session createSession() {
        return new AndroidSpellCheckerSession();
    }

    private static class SuggestionsGatherer implements WordCallback {
        private final int DEFAULT_SUGGESTION_LENGTH = 16;
        private final ArrayList<CharSequence> mSuggestions;
        private final int[] mScores;
        private final int mMaxLength;
        private int mLength = 0;
        private boolean mSeenSuggestions = false;

        SuggestionsGatherer(final int maxLength) {
            mMaxLength = maxLength;
            mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
            mScores = new int[mMaxLength];
        }

        @Override
        synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
                int dicTypeId, DataType dataType) {
            final int positionIndex = ArraysCompatUtils.binarySearch(mScores, 0, mLength, score);
            // binarySearch returns the index if the element exists, and -<insertion index> - 1
            // if it doesn't. See documentation for binarySearch.
            final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;

            mSeenSuggestions = true;
            if (mLength < mMaxLength) {
                final int copyLen = mLength - insertIndex;
                ++mLength;
                System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen);
                mSuggestions.add(insertIndex, new String(word, wordOffset, wordLength));
            } else {
                if (insertIndex == 0) return true;
                System.arraycopy(mScores, 1, mScores, 0, insertIndex);
                mSuggestions.add(insertIndex, new String(word, wordOffset, wordLength));
                mSuggestions.remove(0);
            }
            mScores[insertIndex] = score;

            return true;
        }

        public String[] getGatheredSuggestions() {
            if (!mSeenSuggestions) return null;
            if (0 == mLength) return EMPTY_STRING_ARRAY;

            if (DBG) {
                if (mLength != mSuggestions.size()) {
                    Log.e(TAG, "Suggestion size is not the same as stored mLength");
                }
            }
            Collections.reverse(mSuggestions);
            Utils.removeDupes(mSuggestions);
            // This returns a String[], while toArray() returns an Object[] which cannot be cast
            // into a String[].
            return mSuggestions.toArray(EMPTY_STRING_ARRAY);
        }
    }

    @Override
    public boolean onUnbind(final Intent intent) {
        final Map<String, DictionaryPool> oldPools = mDictionaryPools;
        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
        final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
        mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
        for (DictionaryPool pool : oldPools.values()) {
            pool.close();
        }
        for (Dictionary dict : oldUserDictionaries.values()) {
            dict.close();
        }
        return false;
    }

    private DictionaryPool getDictionaryPool(final String locale) {
        DictionaryPool pool = mDictionaryPools.get(locale);
        if (null == pool) {
            final Locale localeObject = LocaleUtils.constructLocaleFromString(locale);
            pool = new DictionaryPool(POOL_SIZE, this, localeObject);
            mDictionaryPools.put(locale, pool);
        }
        return pool;
    }

    public DictAndProximity createDictAndProximity(final Locale locale) {
        final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo();
        final Resources resources = getResources();
        final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources);
        final DictionaryCollection dictionaryCollection =
                DictionaryFactory.createDictionaryFromManager(this, locale, fallbackResourceId);
        final String localeStr = locale.toString();
        Dictionary userDict = mUserDictionaries.get(localeStr);
        if (null == userDict) {
            userDict = new SynchronouslyLoadedUserDictionary(this, localeStr);
            mUserDictionaries.put(localeStr, userDict);
        }
        dictionaryCollection.addDictionary(userDict);
        return new DictAndProximity(dictionaryCollection, proximityInfo);
    }

    private class AndroidSpellCheckerSession extends Session {
        // Immutable, but need the locale which is not available in the constructor yet
        DictionaryPool mDictionaryPool;
        // Likewise
        Locale mLocale;

        @Override
        public void onCreate() {
            final String localeString = getLocale();
            mDictionaryPool = getDictionaryPool(localeString);
            mLocale = LocaleUtils.constructLocaleFromString(localeString);
        }

        // Note : this must be reentrant
        /**
         * Gets a list of suggestions for a specific string. This returns a list of possible
         * corrections for the text passed as an argument. It may split or group words, and
         * even perform grammatical analysis.
         */
        @Override
        public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
                final int suggestionsLimit) {
            final String text = textInfo.getText();

            if (TextUtils.isEmpty(text)) return EMPTY_SUGGESTIONS_INFO;

            final SuggestionsGatherer suggestionsGatherer =
                    new SuggestionsGatherer(suggestionsLimit);
            final WordComposer composer = new WordComposer();
            final int length = text.length();
            for (int i = 0; i < length; ++i) {
                final int character = text.codePointAt(i);
                final int proximityIndex = SpellCheckerProximityInfo.getIndexOf(character);
                final int[] proximities;
                if (-1 == proximityIndex) {
                    proximities = new int[] { character };
                } else {
                    proximities = Arrays.copyOfRange(SpellCheckerProximityInfo.PROXIMITY,
                            proximityIndex, proximityIndex + SpellCheckerProximityInfo.ROW_SIZE);
                }
                composer.add(character, proximities,
                        WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
            }

            boolean isInDict = true;
            try {
                final DictAndProximity dictInfo = mDictionaryPool.take();
                dictInfo.mDictionary.getWords(composer, suggestionsGatherer,
                        dictInfo.mProximityInfo);
                isInDict = dictInfo.mDictionary.isValidWord(text);
                if (!isInDict && Character.isUpperCase(text.codePointAt(0))) {
                    // If the first char is not uppercase, then the word is either all lower case,
                    // in which case we already tested it, or mixed case, in which case we don't
                    // want to test a lower-case version of it. Hence the test above.
                    // Also note that by isEmpty() test at the top of the method codePointAt(0) is
                    // guaranteed to be there.
                    final int len = text.codePointCount(0, text.length());
                    int capsCount = 1;
                    for (int i = 1; i < len; ++i) {
                        if (1 != capsCount && i != capsCount) break;
                        if (Character.isUpperCase(text.codePointAt(i))) ++capsCount;
                    }
                    // We know the first char is upper case. So we want to test if either everything
                    // else is lower case, or if everything else is upper case. If the string is
                    // exactly one char long, then we will arrive here with capsCount 0, and this is
                    // correct, too.
                    if (1 == capsCount || len == capsCount) {
                        isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
                    }
                }
                if (!mDictionaryPool.offer(dictInfo)) {
                    Log.e(TAG, "Can't re-insert a dictionary into its pool");
                }
            } catch (InterruptedException e) {
                // I don't think this can happen.
                return EMPTY_SUGGESTIONS_INFO;
            }

            final String[] suggestions = suggestionsGatherer.getGatheredSuggestions();

            final int flags =
                    (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY : 0)
                            | (null != suggestions
                                    ? SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO : 0);
            return new SuggestionsInfo(flags, suggestions);
        }
    }
}