AndroidSpellCheckerService.java revision 88fa53b840686bb428b932eed7dd38162ae902c2
1022c1cc20379767966f4915e2dea65fc0b67c0d8satok/* 2022c1cc20379767966f4915e2dea65fc0b67c0d8satok * Copyright (C) 2011 The Android Open Source Project 3022c1cc20379767966f4915e2dea65fc0b67c0d8satok * 4022c1cc20379767966f4915e2dea65fc0b67c0d8satok * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5022c1cc20379767966f4915e2dea65fc0b67c0d8satok * use this file except in compliance with the License. You may obtain a copy of 6022c1cc20379767966f4915e2dea65fc0b67c0d8satok * the License at 7022c1cc20379767966f4915e2dea65fc0b67c0d8satok * 8022c1cc20379767966f4915e2dea65fc0b67c0d8satok * http://www.apache.org/licenses/LICENSE-2.0 9022c1cc20379767966f4915e2dea65fc0b67c0d8satok * 10022c1cc20379767966f4915e2dea65fc0b67c0d8satok * Unless required by applicable law or agreed to in writing, software 11022c1cc20379767966f4915e2dea65fc0b67c0d8satok * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12022c1cc20379767966f4915e2dea65fc0b67c0d8satok * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13022c1cc20379767966f4915e2dea65fc0b67c0d8satok * License for the specific language governing permissions and limitations under 14022c1cc20379767966f4915e2dea65fc0b67c0d8satok * the License. 15022c1cc20379767966f4915e2dea65fc0b67c0d8satok */ 16022c1cc20379767966f4915e2dea65fc0b67c0d8satok 17022c1cc20379767966f4915e2dea65fc0b67c0d8satokpackage com.android.inputmethod.latin.spellcheck; 18022c1cc20379767966f4915e2dea65fc0b67c0d8satok 19c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalardimport android.content.Intent; 203234123fba901243990972158d023a5d1c273316Jean Chalardimport android.content.res.Resources; 21022c1cc20379767966f4915e2dea65fc0b67c0d8satokimport android.service.textservice.SpellCheckerService; 225bcf8ee66ceb38675a6b70fefcb574978e0fae92satokimport android.service.textservice.SpellCheckerService.Session; 23a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalardimport android.util.Log; 24022c1cc20379767966f4915e2dea65fc0b67c0d8satokimport android.view.textservice.SuggestionsInfo; 25022c1cc20379767966f4915e2dea65fc0b67c0d8satokimport android.view.textservice.TextInfo; 265d4c5692f11958064ba7c0de5715f30c96175400Jean Chalardimport android.text.TextUtils; 27022c1cc20379767966f4915e2dea65fc0b67c0d8satok 283234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.compat.ArraysCompatUtils; 293234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.keyboard.Key; 303234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.keyboard.ProximityInfo; 313234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.Dictionary; 323234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.Dictionary.DataType; 333234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.Dictionary.WordCallback; 34150bad6fd4b401177c480acf5640b4db0f821886Jean Chalardimport com.android.inputmethod.latin.DictionaryCollection; 353234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.DictionaryFactory; 36ef35cb631c45c8b106fe7ed9e0d1178c3e5fb963Jean Chalardimport com.android.inputmethod.latin.LocaleUtils; 3759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalardimport com.android.inputmethod.latin.R; 38f019d505d7da97c03c321eef02c4879c4e0448f6Jean Chalardimport com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary; 39150bad6fd4b401177c480acf5640b4db0f821886Jean Chalardimport com.android.inputmethod.latin.UserDictionary; 403234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.Utils; 413234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.WordComposer; 423234123fba901243990972158d023a5d1c273316Jean Chalard 436b166a193398554694cb680f704c2ffc23d03a0eJean Chalardimport java.util.ArrayList; 44f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalardimport java.util.Arrays; 453234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Collections; 463234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Locale; 473234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Map; 483234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.TreeMap; 493234123fba901243990972158d023a5d1c273316Jean Chalard 50022c1cc20379767966f4915e2dea65fc0b67c0d8satok/** 51022c1cc20379767966f4915e2dea65fc0b67c0d8satok * Service for spell checking, using LatinIME's dictionaries and mechanisms. 52022c1cc20379767966f4915e2dea65fc0b67c0d8satok */ 53022c1cc20379767966f4915e2dea65fc0b67c0d8satokpublic class AndroidSpellCheckerService extends SpellCheckerService { 54a90992e56244a914195daba3a2dd8a0e66e63384satok private static final String TAG = AndroidSpellCheckerService.class.getSimpleName(); 55a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard private static final boolean DBG = false; 56a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard private static final int POOL_SIZE = 2; 573234123fba901243990972158d023a5d1c273316Jean Chalard 58f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case 59f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard private static final int CAPITALIZE_FIRST = 1; // First only 60f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard private static final int CAPITALIZE_ALL = 2; // All caps 61f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard 626b166a193398554694cb680f704c2ffc23d03a0eJean Chalard private final static String[] EMPTY_STRING_ARRAY = new String[0]; 635d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard private final static SuggestionsInfo EMPTY_SUGGESTIONS_INFO = 646b166a193398554694cb680f704c2ffc23d03a0eJean Chalard new SuggestionsInfo(0, EMPTY_STRING_ARRAY); 65c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard private Map<String, DictionaryPool> mDictionaryPools = 66a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard Collections.synchronizedMap(new TreeMap<String, DictionaryPool>()); 67150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard private Map<String, Dictionary> mUserDictionaries = 68150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard Collections.synchronizedMap(new TreeMap<String, Dictionary>()); 693234123fba901243990972158d023a5d1c273316Jean Chalard 7059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard private double mTypoThreshold; 7159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard 7259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard @Override public void onCreate() { 7359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard super.onCreate(); 7459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard mTypoThreshold = Double.parseDouble(getString(R.string.spellchecker_typo_threshold_value)); 7559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 7659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard 775bcf8ee66ceb38675a6b70fefcb574978e0fae92satok @Override 785bcf8ee66ceb38675a6b70fefcb574978e0fae92satok public Session createSession() { 7959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard return new AndroidSpellCheckerSession(this); 805bcf8ee66ceb38675a6b70fefcb574978e0fae92satok } 815bcf8ee66ceb38675a6b70fefcb574978e0fae92satok 823234123fba901243990972158d023a5d1c273316Jean Chalard private static class SuggestionsGatherer implements WordCallback { 8359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard public static class Result { 8459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard public final String[] mSuggestions; 8559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard public final boolean mLooksLikeTypo; 8659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard public Result(final String[] gatheredSuggestions, final boolean looksLikeTypo) { 8759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard mSuggestions = gatheredSuggestions; 8859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard mLooksLikeTypo = looksLikeTypo; 8959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 9059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 9159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard 923234123fba901243990972158d023a5d1c273316Jean Chalard private final int DEFAULT_SUGGESTION_LENGTH = 16; 936b166a193398554694cb680f704c2ffc23d03a0eJean Chalard private final ArrayList<CharSequence> mSuggestions; 943234123fba901243990972158d023a5d1c273316Jean Chalard private final int[] mScores; 953234123fba901243990972158d023a5d1c273316Jean Chalard private final int mMaxLength; 963234123fba901243990972158d023a5d1c273316Jean Chalard private int mLength = 0; 9759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard 9859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // The two following attributes are only ever filled if the requested max length 9959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // is 0 (or less, which is treated the same). 10059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard private String mBestSuggestion = null; 10159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard private int mBestScore = Integer.MIN_VALUE; // As small as possible 1023234123fba901243990972158d023a5d1c273316Jean Chalard 1033234123fba901243990972158d023a5d1c273316Jean Chalard SuggestionsGatherer(final int maxLength) { 1043234123fba901243990972158d023a5d1c273316Jean Chalard mMaxLength = maxLength; 1056b166a193398554694cb680f704c2ffc23d03a0eJean Chalard mSuggestions = new ArrayList<CharSequence>(maxLength + 1); 1063234123fba901243990972158d023a5d1c273316Jean Chalard mScores = new int[mMaxLength]; 1073234123fba901243990972158d023a5d1c273316Jean Chalard } 1083234123fba901243990972158d023a5d1c273316Jean Chalard 1093234123fba901243990972158d023a5d1c273316Jean Chalard @Override 1103234123fba901243990972158d023a5d1c273316Jean Chalard synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score, 1113234123fba901243990972158d023a5d1c273316Jean Chalard int dicTypeId, DataType dataType) { 1123234123fba901243990972158d023a5d1c273316Jean Chalard final int positionIndex = ArraysCompatUtils.binarySearch(mScores, 0, mLength, score); 1133234123fba901243990972158d023a5d1c273316Jean Chalard // binarySearch returns the index if the element exists, and -<insertion index> - 1 1143234123fba901243990972158d023a5d1c273316Jean Chalard // if it doesn't. See documentation for binarySearch. 1153234123fba901243990972158d023a5d1c273316Jean Chalard final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1; 1163234123fba901243990972158d023a5d1c273316Jean Chalard 1173234123fba901243990972158d023a5d1c273316Jean Chalard if (mLength < mMaxLength) { 1183234123fba901243990972158d023a5d1c273316Jean Chalard final int copyLen = mLength - insertIndex; 1193234123fba901243990972158d023a5d1c273316Jean Chalard ++mLength; 1203234123fba901243990972158d023a5d1c273316Jean Chalard System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen); 1216b166a193398554694cb680f704c2ffc23d03a0eJean Chalard mSuggestions.add(insertIndex, new String(word, wordOffset, wordLength)); 1223234123fba901243990972158d023a5d1c273316Jean Chalard } else { 12359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard if (insertIndex == 0) { 12459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // If the maxLength is 0 (should never be less, but if it is, it's treated as 0) 12559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // then we need to keep track of the best suggestion in mBestScore and 12659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // mBestSuggestion. This is so that we know whether the best suggestion makes 12759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // the score cutoff, since we need to know that to return a meaningful 12859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // looksLikeTypo. 12959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard if (0 >= mMaxLength) { 13059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard if (score > mBestScore) { 13159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard mBestScore = score; 13259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard mBestSuggestion = new String(word, wordOffset, wordLength); 13359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 13459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 13559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard return true; 13659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 1373234123fba901243990972158d023a5d1c273316Jean Chalard System.arraycopy(mScores, 1, mScores, 0, insertIndex); 1386b166a193398554694cb680f704c2ffc23d03a0eJean Chalard mSuggestions.add(insertIndex, new String(word, wordOffset, wordLength)); 1396b166a193398554694cb680f704c2ffc23d03a0eJean Chalard mSuggestions.remove(0); 1403234123fba901243990972158d023a5d1c273316Jean Chalard } 1413234123fba901243990972158d023a5d1c273316Jean Chalard mScores[insertIndex] = score; 1423234123fba901243990972158d023a5d1c273316Jean Chalard 1433234123fba901243990972158d023a5d1c273316Jean Chalard return true; 1443234123fba901243990972158d023a5d1c273316Jean Chalard } 1453234123fba901243990972158d023a5d1c273316Jean Chalard 146f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard public Result getResults(final CharSequence originalText, final double threshold, 147f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard final int capitalizeType, final Locale locale) { 14859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard final String[] gatheredSuggestions; 14959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard final boolean looksLikeTypo; 15059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard if (0 == mLength) { 15159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // Either we found no suggestions, or we found some BUT the max length was 0. 15259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // If we found some mBestSuggestion will not be null. If it is null, then 15359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // we found none, regardless of the max length. 15459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard if (null == mBestSuggestion) { 15559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard gatheredSuggestions = null; 15659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard looksLikeTypo = false; 15759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } else { 15859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard gatheredSuggestions = EMPTY_STRING_ARRAY; 15959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard final double normalizedScore = 16059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard Utils.calcNormalizedScore(originalText, mBestSuggestion, mBestScore); 16159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard looksLikeTypo = (normalizedScore > threshold); 16259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 16359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } else { 16459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard if (DBG) { 16559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard if (mLength != mSuggestions.size()) { 16659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard Log.e(TAG, "Suggestion size is not the same as stored mLength"); 16759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 168af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard for (int i = mLength - 1; i >= 0; --i) { 169af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard Log.i(TAG, "" + mScores[i] + " " + mSuggestions.get(i)); 170af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard } 1716b166a193398554694cb680f704c2ffc23d03a0eJean Chalard } 17259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard Collections.reverse(mSuggestions); 17359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard Utils.removeDupes(mSuggestions); 174f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard if (CAPITALIZE_ALL == capitalizeType) { 175f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard for (int i = 0; i < mSuggestions.size(); ++i) { 176f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // get(i) returns a CharSequence which is actually a String so .toString() 177f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // should return the same object. 178f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale)); 179f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard } 180f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard } else if (CAPITALIZE_FIRST == capitalizeType) { 181f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard for (int i = 0; i < mSuggestions.size(); ++i) { 182f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // Likewise 183f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard mSuggestions.set(i, Utils.toTitleCase(mSuggestions.get(i).toString(), 184f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard locale)); 185f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard } 186f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard } 18759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // This returns a String[], while toArray() returns an Object[] which cannot be cast 18859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard // into a String[]. 18959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard gatheredSuggestions = mSuggestions.toArray(EMPTY_STRING_ARRAY); 19059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard 191af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard final int bestScore = mScores[mLength - 1]; 19259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard final CharSequence bestSuggestion = mSuggestions.get(0); 19359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard final double normalizedScore = 19459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard Utils.calcNormalizedScore(originalText, bestSuggestion, bestScore); 19559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard looksLikeTypo = (normalizedScore > threshold); 196af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard if (DBG) { 197af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore); 198af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard Log.i(TAG, "Normalized score = " + normalizedScore + " (threshold " + threshold 199af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard + ") => looksLikeTypo = " + looksLikeTypo); 200af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard } 2013234123fba901243990972158d023a5d1c273316Jean Chalard } 20259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard return new Result(gatheredSuggestions, looksLikeTypo); 2033234123fba901243990972158d023a5d1c273316Jean Chalard } 2043234123fba901243990972158d023a5d1c273316Jean Chalard } 2053234123fba901243990972158d023a5d1c273316Jean Chalard 206c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard @Override 207c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard public boolean onUnbind(final Intent intent) { 208c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard final Map<String, DictionaryPool> oldPools = mDictionaryPools; 209c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>()); 210150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries; 211150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>()); 212c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard for (DictionaryPool pool : oldPools.values()) { 213c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard pool.close(); 214c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard } 215150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard for (Dictionary dict : oldUserDictionaries.values()) { 216150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard dict.close(); 217150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard } 218c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard return false; 219c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard } 220c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard 221a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard private DictionaryPool getDictionaryPool(final String locale) { 222a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard DictionaryPool pool = mDictionaryPools.get(locale); 223a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard if (null == pool) { 224ef35cb631c45c8b106fe7ed9e0d1178c3e5fb963Jean Chalard final Locale localeObject = LocaleUtils.constructLocaleFromString(locale); 225a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard pool = new DictionaryPool(POOL_SIZE, this, localeObject); 226a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard mDictionaryPools.put(locale, pool); 2273234123fba901243990972158d023a5d1c273316Jean Chalard } 228a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard return pool; 229a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard } 230a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard 231a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard public DictAndProximity createDictAndProximity(final Locale locale) { 232a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo(); 233a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard final Resources resources = getResources(); 234a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard final int fallbackResourceId = Utils.getMainDictionaryResourceId(resources); 235150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard final DictionaryCollection dictionaryCollection = 236a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard DictionaryFactory.createDictionaryFromManager(this, locale, fallbackResourceId); 237150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard final String localeStr = locale.toString(); 238150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard Dictionary userDict = mUserDictionaries.get(localeStr); 239150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard if (null == userDict) { 240f019d505d7da97c03c321eef02c4879c4e0448f6Jean Chalard userDict = new SynchronouslyLoadedUserDictionary(this, localeStr); 241150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard mUserDictionaries.put(localeStr, userDict); 242150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard } 243150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard dictionaryCollection.addDictionary(userDict); 244150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard return new DictAndProximity(dictionaryCollection, proximityInfo); 2453234123fba901243990972158d023a5d1c273316Jean Chalard } 2463234123fba901243990972158d023a5d1c273316Jean Chalard 247f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // This method assumes the text is not empty or null. 248f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard private static int getCapitalizationType(String text) { 249f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // If the first char is not uppercase, then the word is either all lower case, 250f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // and in either case we return CAPITALIZE_NONE. 251f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE; 252f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard final int len = text.codePointCount(0, text.length()); 253f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard int capsCount = 1; 254f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard for (int i = 1; i < len; ++i) { 255f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard if (1 != capsCount && i != capsCount) break; 256f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard if (Character.isUpperCase(text.codePointAt(i))) ++capsCount; 257f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard } 258f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // We know the first char is upper case. So we want to test if either everything 259f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // else is lower case, or if everything else is upper case. If the string is 260f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // exactly one char long, then we will arrive here with capsCount 1, and this is 261f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // correct, too. 262f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard if (1 == capsCount) return CAPITALIZE_FIRST; 263f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE); 264f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard } 265f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard 26659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard private static class AndroidSpellCheckerSession extends Session { 267a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard // Immutable, but need the locale which is not available in the constructor yet 26859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard private DictionaryPool mDictionaryPool; 2695d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard // Likewise 27059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard private Locale mLocale; 27159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard 27259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard private final AndroidSpellCheckerService mService; 27359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard 27459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard AndroidSpellCheckerSession(final AndroidSpellCheckerService service) { 27559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard mService = service; 27659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard } 277a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard 2785bcf8ee66ceb38675a6b70fefcb574978e0fae92satok @Override 2795bcf8ee66ceb38675a6b70fefcb574978e0fae92satok public void onCreate() { 2805d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard final String localeString = getLocale(); 28159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard mDictionaryPool = mService.getDictionaryPool(localeString); 282ef35cb631c45c8b106fe7ed9e0d1178c3e5fb963Jean Chalard mLocale = LocaleUtils.constructLocaleFromString(localeString); 283a90992e56244a914195daba3a2dd8a0e66e63384satok } 2843234123fba901243990972158d023a5d1c273316Jean Chalard 28588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard /** 28688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard * Finds out whether a particular string should be filtered out of spell checking. 28788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard * 28888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard * This will loosely match URLs, numbers, symbols. 28988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard * 29088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard * @param text the string to evaluate. 29188fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard * @return true if we should filter this text out, false otherwise 29288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard */ 29388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard private boolean shouldFilterOut(final String text) { 29488fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard if (TextUtils.isEmpty(text) || text.length() <= 1) return true; 29588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard 29688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // TODO: check if an equivalent processing can't be done more quickly with a 29788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // compiled regexp. 29888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // Filter by first letter 29988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard final int firstCodePoint = text.codePointAt(0); 30088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // Filter out words that don't start with a letter or an apostrophe 30188fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard if (!Character.isLetter(firstCodePoint) 30288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard && '\'' != firstCodePoint) return true; 30388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard 30488fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // Filter contents 30588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard final int length = text.length(); 30688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard int letterCount = 0; 30788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard for (int i = 0; i < length; ++i) { 30888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard final int codePoint = text.codePointAt(i); 30988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // Any word containing a '@' is probably an e-mail address 31088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // Any word containing a '/' is probably either an ad-hoc combination of two 31188fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // words or a URI - in either case we don't want to spell check that 31288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard if ('@' == codePoint 31388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard || '/' == codePoint) return true; 31488fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard if (Character.isLetter(codePoint)) ++letterCount; 31588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard } 31688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // Guestimate heuristic: perform spell checking if at least 3/4 of the characters 31788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard // in this word are letters 31888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard return (letterCount * 4 < length * 3); 31988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard } 32088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard 3215bcf8ee66ceb38675a6b70fefcb574978e0fae92satok // Note : this must be reentrant 3225bcf8ee66ceb38675a6b70fefcb574978e0fae92satok /** 3235bcf8ee66ceb38675a6b70fefcb574978e0fae92satok * Gets a list of suggestions for a specific string. This returns a list of possible 32470b9c5d9913b676f21fe29f795bdb25324509205Jean Chalard * corrections for the text passed as an argument. It may split or group words, and 3255bcf8ee66ceb38675a6b70fefcb574978e0fae92satok * even perform grammatical analysis. 3265bcf8ee66ceb38675a6b70fefcb574978e0fae92satok */ 3275bcf8ee66ceb38675a6b70fefcb574978e0fae92satok @Override 3285bcf8ee66ceb38675a6b70fefcb574978e0fae92satok public SuggestionsInfo onGetSuggestions(final TextInfo textInfo, 3295bcf8ee66ceb38675a6b70fefcb574978e0fae92satok final int suggestionsLimit) { 3305bcf8ee66ceb38675a6b70fefcb574978e0fae92satok final String text = textInfo.getText(); 3315bcf8ee66ceb38675a6b70fefcb574978e0fae92satok 33288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard if (shouldFilterOut(text)) return EMPTY_SUGGESTIONS_INFO; 3335d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard 3345bcf8ee66ceb38675a6b70fefcb574978e0fae92satok final SuggestionsGatherer suggestionsGatherer = 3355bcf8ee66ceb38675a6b70fefcb574978e0fae92satok new SuggestionsGatherer(suggestionsLimit); 3365bcf8ee66ceb38675a6b70fefcb574978e0fae92satok final WordComposer composer = new WordComposer(); 3375bcf8ee66ceb38675a6b70fefcb574978e0fae92satok final int length = text.length(); 3385bcf8ee66ceb38675a6b70fefcb574978e0fae92satok for (int i = 0; i < length; ++i) { 339f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard final int character = text.codePointAt(i); 340f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard final int proximityIndex = SpellCheckerProximityInfo.getIndexOf(character); 341f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard final int[] proximities; 342f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard if (-1 == proximityIndex) { 343f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard proximities = new int[] { character }; 344f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard } else { 345f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard proximities = Arrays.copyOfRange(SpellCheckerProximityInfo.PROXIMITY, 346f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard proximityIndex, proximityIndex + SpellCheckerProximityInfo.ROW_SIZE); 347f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard } 348f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalard composer.add(character, proximities, 3495bcf8ee66ceb38675a6b70fefcb574978e0fae92satok WordComposer.NOT_A_COORDINATE, WordComposer.NOT_A_COORDINATE); 3505bcf8ee66ceb38675a6b70fefcb574978e0fae92satok } 351a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard 352f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard final int capitalizeType = getCapitalizationType(text); 353a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard boolean isInDict = true; 354a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard try { 355a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard final DictAndProximity dictInfo = mDictionaryPool.take(); 356a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard dictInfo.mDictionary.getWords(composer, suggestionsGatherer, 357a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard dictInfo.mProximityInfo); 358a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard isInDict = dictInfo.mDictionary.isValidWord(text); 359f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard if (!isInDict && CAPITALIZE_NONE != capitalizeType) { 360f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // We want to test the word again if it's all caps or first caps only. 361f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // If it's fully down, we already tested it, if it's mixed case, we don't 362f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard // want to test a lowercase version of it. 363f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale)); 3645d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard } 365c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard if (!mDictionaryPool.offer(dictInfo)) { 366c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard Log.e(TAG, "Can't re-insert a dictionary into its pool"); 367c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard } 368a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard } catch (InterruptedException e) { 369a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard // I don't think this can happen. 3705d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard return EMPTY_SUGGESTIONS_INFO; 37170b9c5d9913b676f21fe29f795bdb25324509205Jean Chalard } 3725bcf8ee66ceb38675a6b70fefcb574978e0fae92satok 373f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(text, 374f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard mService.mTypoThreshold, capitalizeType, mLocale); 375a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard 376af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard if (DBG) { 377af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard Log.i(TAG, "Spell checking results for " + text + " with suggestion limit " 378af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard + suggestionsLimit); 379af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard Log.i(TAG, "IsInDict = " + result.mLooksLikeTypo); 380af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard Log.i(TAG, "LooksLikeTypo = " + result.mLooksLikeTypo); 381af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard for (String suggestion : result.mSuggestions) { 382af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard Log.i(TAG, suggestion); 383af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard } 384af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard } 385af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard 3865bcf8ee66ceb38675a6b70fefcb574978e0fae92satok final int flags = 3875bcf8ee66ceb38675a6b70fefcb574978e0fae92satok (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY : 0) 38859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard | (result.mLooksLikeTypo 3895bcf8ee66ceb38675a6b70fefcb574978e0fae92satok ? SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO : 0); 39059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard return new SuggestionsInfo(flags, result.mSuggestions); 3915bcf8ee66ceb38675a6b70fefcb574978e0fae92satok } 392022c1cc20379767966f4915e2dea65fc0b67c0d8satok } 393022c1cc20379767966f4915e2dea65fc0b67c0d8satok} 394