diff options
Diffstat (limited to 'java/src/com/android/inputmethod/latin/spellcheck')
3 files changed, 261 insertions, 115 deletions
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java new file mode 100644 index 000000000..44e999572 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java @@ -0,0 +1,167 @@ +/* + * 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.res.Resources; +import android.service.textservice.SpellCheckerService; +import android.service.textservice.SpellCheckerService.Session; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; + +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.DictionaryFactory; +import com.android.inputmethod.latin.Utils; +import com.android.inputmethod.latin.WordComposer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.LinkedList; +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 = true; + + private final static String[] emptyArray = new String[0]; + private final ProximityInfo mProximityInfo = ProximityInfo.getSpellCheckerProximityInfo(); + private final Map<String, Dictionary> mDictionaries = + 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 String[] mSuggestions; + private final int[] mScores; + private final int mMaxLength; + private int mLength = 0; + + SuggestionsGatherer(final int maxLength) { + mMaxLength = maxLength; + mSuggestions = new String[mMaxLength]; + 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; + + if (mLength < mMaxLength) { + final int copyLen = mLength - insertIndex; + ++mLength; + System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen); + System.arraycopy(mSuggestions, insertIndex, mSuggestions, insertIndex + 1, copyLen); + } else { + if (insertIndex == 0) return true; + System.arraycopy(mScores, 1, mScores, 0, insertIndex); + System.arraycopy(mSuggestions, 1, mSuggestions, 0, insertIndex); + } + mScores[insertIndex] = score; + mSuggestions[insertIndex] = new String(word, wordOffset, wordLength); + + return true; + } + + public String[] getGatheredSuggestions() { + if (0 == mLength) return null; + + final String[] results = new String[mLength]; + for (int i = mLength - 1; i >= 0; --i) { + results[mLength - i - 1] = mSuggestions[i]; + } + return results; + } + } + + private Dictionary getDictionary(final String locale) { + Dictionary dictionary = mDictionaries.get(locale); + if (null == dictionary) { + final Resources resources = getResources(); + final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources); + final Locale localeObject = Utils.constructLocaleFromString(locale); + dictionary = DictionaryFactory.createDictionaryFromManager(this, localeObject, + fallbackResourceId); + mDictionaries.put(locale, dictionary); + } + return dictionary; + } + + private class AndroidSpellCheckerSession extends Session { + @Override + public void onCreate() { + } + + // 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 arguments. It may split or group words, and + * even perform grammatical analysis. + */ + @Override + public SuggestionsInfo onGetSuggestions(final TextInfo textInfo, + final int suggestionsLimit) { + final String locale = getLocale(); + final Dictionary dictionary = getDictionary(locale); + final String text = textInfo.getText(); + + 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); + } + dictionary.getWords(composer, suggestionsGatherer, mProximityInfo); + final boolean isInDict = dictionary.isValidWord(text); + 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); + } + } +} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java deleted file mode 100644 index 63c6d69d7..000000000 --- a/java/src/com/android/inputmethod/latin/spellcheck/SpellChecker.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.Context; -import android.content.res.Resources; - -import com.android.inputmethod.compat.ArraysCompatUtils; -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.DictionaryFactory; -import com.android.inputmethod.latin.Utils; -import com.android.inputmethod.latin.WordComposer; - -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; - -/** - * Implements spell checking methods. - */ -public class SpellChecker { - - public final Dictionary mDictionary; - - public SpellChecker(final Context context, final Locale locale) { - final Resources resources = context.getResources(); - final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources); - mDictionary = DictionaryFactory.createDictionaryFromManager(context, locale, - fallbackResourceId); - } - - // Note : this must be reentrant - /** - * Finds out whether a word is in the dictionary or not. - * - * @param text the sequence containing the word to check for. - * @param start the index of the first character of the word in text. - * @param end the index of the next-to-last character in text. - * @return true if the word is in the dictionary, false otherwise. - */ - public boolean isCorrect(final CharSequence text, final int start, final int end) { - return mDictionary.isValidWord(text.subSequence(start, end)); - } - - private static class SuggestionsGatherer implements WordCallback { - private final int DEFAULT_SUGGESTION_LENGTH = 16; - private final List<String> mSuggestions = new LinkedList<String>(); - private int[] mScores = new int[DEFAULT_SUGGESTION_LENGTH]; - private int mLength = 0; - - @Override - synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score, - int dicTypeId, DataType dataType) { - if (mLength >= mScores.length) { - final int newLength = mScores.length * 2; - mScores = new int[newLength]; - } - 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 insertionIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1; - System.arraycopy(mScores, insertionIndex, mScores, insertionIndex + 1, - mLength - insertionIndex); - mLength += 1; - mScores[insertionIndex] = score; - mSuggestions.add(insertionIndex, new String(word, wordOffset, wordLength)); - return true; - } - - public List<String> getGatheredSuggestions() { - return mSuggestions; - } - } - - // 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 - * arguments. It may split or group words, and even perform grammatical - * analysis. - * - * @param text the sequence containing the word to check for. - * @param start the index of the first character of the word in text. - * @param end the index of the next-to-last character in text. - * @return a list of possible suggestions to replace the text. - */ - public List<String> getSuggestions(final CharSequence text, final int start, final int end) { - final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(); - final WordComposer composer = new WordComposer(); - for (int i = start; i < end; ++i) { - int character = text.charAt(i); - composer.add(character, new int[] { character }, - WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); - } - mDictionary.getWords(composer, suggestionsGatherer); - return suggestionsGatherer.getGatheredSuggestions(); - } -} diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java new file mode 100644 index 000000000..abcf7e52a --- /dev/null +++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java @@ -0,0 +1,94 @@ +/* + * 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 com.android.inputmethod.keyboard.KeyDetector; +import com.android.inputmethod.keyboard.ProximityInfo; + +import java.util.Map; +import java.util.TreeMap; + +public class SpellCheckerProximityInfo { + final private static int NUL = KeyDetector.NOT_A_CODE; + + // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside + // native code - this value is passed at creation of the binary object and reused + // as the size of the passed array afterwards so they can't be different. + final public static int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE; + + // This is a map from the code point to the index in the PROXIMITY array. + // At the time the native code to read the binary dictionary needs the proximity info be passed + // as a flat array spaced by MAX_PROXIMITY_CHARS_SIZE columns, one for each input character. + // Since we need to build such an array, we want to be able to search in our big proximity data + // quickly by character, and a map is probably the best way to do this. + final private static TreeMap<Integer, Integer> INDICES = new TreeMap<Integer, Integer>(); + + // The proximity here is the union of + // - the proximity for a QWERTY keyboard. + // - the proximity for an AZERTY keyboard. + // - the proximity for a QWERTZ keyboard. + // ...plus, add all characters in the ('a', 'e', 'i', 'o', 'u') set to each other. + // + // The reasoning behind this construction is, almost any alphabetic text we may want + // to spell check has been entered with one of the keyboards above. Also, specifically + // to English, many spelling errors consist of the last vowel of the word being wrong + // because in English vowels tend to merge with each other in pronunciation. + final public static int[] PROXIMITY = { + 'q', 'w', 's', 'a', 'z', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'w', 'q', 'a', 's', 'd', 'e', 'x', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'e', 'w', 's', 'd', 'f', 'r', 'a', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL, + 'r', 'e', 'd', 'f', 'g', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 't', 'r', 'f', 'g', 'h', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'y', 't', 'g', 'h', 'j', 'u', 'a', 's', 'd', 'x', NUL, NUL, NUL, NUL, NUL, NUL, + 'u', 'y', 'h', 'j', 'k', 'i', 'a', 'e', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'i', 'u', 'j', 'k', 'l', 'o', 'a', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'o', 'i', 'k', 'l', 'p', 'a', 'e', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'p', 'o', 'l', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + + 'a', 'z', 'x', 's', 'w', 'q', 'e', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL, + 's', 'q', 'a', 'z', 'x', 'c', 'd', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'd', 'w', 's', 'x', 'c', 'v', 'f', 'r', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, + 'f', 'e', 'd', 'c', 'v', 'b', 'g', 't', 'r', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'g', 'r', 'f', 'v', 'b', 'n', 'h', 'y', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'h', 't', 'g', 'b', 'n', 'm', 'j', 'u', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'j', 'y', 'h', 'n', 'm', 'k', 'i', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'k', 'u', 'j', 'm', 'l', 'o', 'i', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'l', 'i', 'k', 'p', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + + 'z', 'a', 's', 'd', 'x', 't', 'g', 'h', 'j', 'u', 'q', 'e', NUL, NUL, NUL, NUL, + 'x', 'z', 'a', 's', 'd', 'c', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'c', 'x', 's', 'd', 'f', 'v', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'v', 'c', 'd', 'f', 'g', 'b', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'b', 'v', 'f', 'g', 'h', 'n', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'n', 'b', 'g', 'h', 'j', 'm', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + 'm', 'n', 'h', 'j', 'k', 'l', 'o', 'p', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, + }; + static { + for (int i = 0; i < PROXIMITY.length; i += ROW_SIZE) { + if (NUL != PROXIMITY[i]) INDICES.put(PROXIMITY[i], i); + } + } + public static int getIndexOf(int characterCode) { + final Integer result = INDICES.get(characterCode); + if (null == result) return -1; + return result; + } +} |