/* * 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 mDictionaryPools = Collections.synchronizedMap(new TreeMap()); private Map mUserDictionaries = Collections.synchronizedMap(new TreeMap()); @Override public Session createSession() { return new AndroidSpellCheckerSession(); } private static class SuggestionsGatherer implements WordCallback { private final int DEFAULT_SUGGESTION_LENGTH = 16; private final ArrayList 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(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 - - 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 oldPools = mDictionaryPools; mDictionaryPools = Collections.synchronizedMap(new TreeMap()); final Map oldUserDictionaries = mUserDictionaries; mUserDictionaries = Collections.synchronizedMap(new TreeMap()); 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); } } }