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