AndroidSpellCheckerService.java revision c160373b6a8e8a536ad8aa2798a33a41d3050f3b
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;
26
27import com.android.inputmethod.compat.ArraysCompatUtils;
28import com.android.inputmethod.keyboard.Key;
29import com.android.inputmethod.keyboard.ProximityInfo;
30import com.android.inputmethod.latin.Dictionary;
31import com.android.inputmethod.latin.Dictionary.DataType;
32import com.android.inputmethod.latin.Dictionary.WordCallback;
33import com.android.inputmethod.latin.DictionaryFactory;
34import com.android.inputmethod.latin.Utils;
35import com.android.inputmethod.latin.WordComposer;
36
37import java.util.Arrays;
38import java.util.Collections;
39import java.util.Locale;
40import java.util.Map;
41import java.util.TreeMap;
42
43/**
44 * Service for spell checking, using LatinIME's dictionaries and mechanisms.
45 */
46public class AndroidSpellCheckerService extends SpellCheckerService {
47    private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
48    private static final boolean DBG = false;
49    private static final int POOL_SIZE = 2;
50
51    private final static String[] emptyArray = new String[0];
52    private Map<String, DictionaryPool> mDictionaryPools =
53            Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
54
55    @Override
56    public Session createSession() {
57        return new AndroidSpellCheckerSession();
58    }
59
60    private static class SuggestionsGatherer implements WordCallback {
61        private final int DEFAULT_SUGGESTION_LENGTH = 16;
62        private final String[] mSuggestions;
63        private final int[] mScores;
64        private final int mMaxLength;
65        private int mLength = 0;
66
67        SuggestionsGatherer(final int maxLength) {
68            mMaxLength = maxLength;
69            mSuggestions = new String[mMaxLength];
70            mScores = new int[mMaxLength];
71        }
72
73        @Override
74        synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
75                int dicTypeId, DataType dataType) {
76            final int positionIndex = ArraysCompatUtils.binarySearch(mScores, 0, mLength, score);
77            // binarySearch returns the index if the element exists, and -<insertion index> - 1
78            // if it doesn't. See documentation for binarySearch.
79            final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
80
81            if (mLength < mMaxLength) {
82                final int copyLen = mLength - insertIndex;
83                ++mLength;
84                System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen);
85                System.arraycopy(mSuggestions, insertIndex, mSuggestions, insertIndex + 1, copyLen);
86            } else {
87                if (insertIndex == 0) return true;
88                System.arraycopy(mScores, 1, mScores, 0, insertIndex);
89                System.arraycopy(mSuggestions, 1, mSuggestions, 0, insertIndex);
90            }
91            mScores[insertIndex] = score;
92            mSuggestions[insertIndex] = new String(word, wordOffset, wordLength);
93
94            return true;
95        }
96
97        public String[] getGatheredSuggestions() {
98            if (0 == mLength) return null;
99
100            final String[] results = new String[mLength];
101            for (int i = mLength - 1; i >= 0; --i) {
102                results[mLength - i - 1] = mSuggestions[i];
103            }
104            return results;
105        }
106    }
107
108    @Override
109    public boolean onUnbind(final Intent intent) {
110        final Map<String, DictionaryPool> oldPools = mDictionaryPools;
111        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
112        for (DictionaryPool pool : oldPools.values()) {
113            pool.close();
114        }
115        return false;
116    }
117
118    private DictionaryPool getDictionaryPool(final String locale) {
119        DictionaryPool pool = mDictionaryPools.get(locale);
120        if (null == pool) {
121            final Locale localeObject = Utils.constructLocaleFromString(locale);
122            pool = new DictionaryPool(POOL_SIZE, this, localeObject);
123            mDictionaryPools.put(locale, pool);
124        }
125        return pool;
126    }
127
128    public DictAndProximity createDictAndProximity(final Locale locale) {
129        final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo();
130        final Resources resources = getResources();
131        final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources);
132        final Dictionary dictionary =
133                DictionaryFactory.createDictionaryFromManager(this, locale, fallbackResourceId);
134        return new DictAndProximity(dictionary, proximityInfo);
135    }
136
137    private class AndroidSpellCheckerSession extends Session {
138        // Immutable, but need the locale which is not available in the constructor yet
139        DictionaryPool mDictionaryPool;
140
141        @Override
142        public void onCreate() {
143            mDictionaryPool = getDictionaryPool(getLocale());
144        }
145
146        // Note : this must be reentrant
147        /**
148         * Gets a list of suggestions for a specific string. This returns a list of possible
149         * corrections for the text passed as an argument. It may split or group words, and
150         * even perform grammatical analysis.
151         */
152        @Override
153        public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
154                final int suggestionsLimit) {
155            final String text = textInfo.getText();
156
157            final SuggestionsGatherer suggestionsGatherer =
158                    new SuggestionsGatherer(suggestionsLimit);
159            final WordComposer composer = new WordComposer();
160            final int length = text.length();
161            for (int i = 0; i < length; ++i) {
162                final int character = text.codePointAt(i);
163                final int proximityIndex = SpellCheckerProximityInfo.getIndexOf(character);
164                final int[] proximities;
165                if (-1 == proximityIndex) {
166                    proximities = new int[] { character };
167                } else {
168                    proximities = Arrays.copyOfRange(SpellCheckerProximityInfo.PROXIMITY,
169                            proximityIndex, proximityIndex + SpellCheckerProximityInfo.ROW_SIZE);
170                }
171                composer.add(character, proximities,
172                        WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE);
173            }
174
175            boolean isInDict = true;
176            try {
177                final DictAndProximity dictInfo = mDictionaryPool.take();
178                dictInfo.mDictionary.getWords(composer, suggestionsGatherer,
179                        dictInfo.mProximityInfo);
180                isInDict = dictInfo.mDictionary.isValidWord(text);
181                if (!mDictionaryPool.offer(dictInfo)) {
182                    Log.e(TAG, "Can't re-insert a dictionary into its pool");
183                }
184            } catch (InterruptedException e) {
185                // I don't think this can happen.
186                return new SuggestionsInfo(0, new String[0]);
187            }
188
189            final String[] suggestions = suggestionsGatherer.getGatheredSuggestions();
190
191            final int flags =
192                    (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY : 0)
193                            | (null != suggestions
194                                    ? SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO : 0);
195            return new SuggestionsInfo(flags, suggestions);
196        }
197    }
198}
199