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