AndroidSpellCheckerService.java revision f0e12a969974987f1b97929886c6ebe6a685c538
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;
20db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalardimport android.content.SharedPreferences;
21db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalardimport android.preference.PreferenceManager;
22022c1cc20379767966f4915e2dea65fc0b67c0d8satokimport android.service.textservice.SpellCheckerService;
23ab72a97d7ce44230a0c824797d1675a5ca354a56Tadashi G. Takaokaimport android.text.TextUtils;
24a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalardimport android.util.Log;
2574a84febc76d1ec6c0b6d8afbf50349da9b38d74satokimport android.util.LruCache;
265434f46481c6331c3f107e6940cb49ba9dd5ea4dsatokimport android.view.textservice.SentenceSuggestionsInfo;
27022c1cc20379767966f4915e2dea65fc0b67c0d8satokimport android.view.textservice.SuggestionsInfo;
28022c1cc20379767966f4915e2dea65fc0b67c0d8satokimport android.view.textservice.TextInfo;
29022c1cc20379767966f4915e2dea65fc0b67c0d8satok
309260422423819ed6942f11c03960d5764e97c262Ken Wakasaimport com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
313234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.keyboard.ProximityInfo;
32673cebf9e97289b3b0cd343ff7193dff69684a48Jean Chalardimport com.android.inputmethod.latin.BinaryDictionary;
333234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.Dictionary;
343234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.Dictionary.WordCallback;
35150bad6fd4b401177c480acf5640b4db0f821886Jean Chalardimport com.android.inputmethod.latin.DictionaryCollection;
363234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.DictionaryFactory;
3718222f8c863e509538857b1fafca9c696fae2f55Tom Ouyangimport com.android.inputmethod.latin.LatinIME;
38ef35cb631c45c8b106fe7ed9e0d1178c3e5fb963Jean Chalardimport com.android.inputmethod.latin.LocaleUtils;
3959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalardimport com.android.inputmethod.latin.R;
40cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaokaimport com.android.inputmethod.latin.StringUtils;
4118222f8c863e509538857b1fafca9c696fae2f55Tom Ouyangimport com.android.inputmethod.latin.SynchronouslyLoadedContactsBinaryDictionary;
422e3c6da8688a907024d4d8e0f2db3e0ed4fab8dbJean Chalardimport com.android.inputmethod.latin.SynchronouslyLoadedContactsDictionary;
43f019d505d7da97c03c321eef02c4879c4e0448f6Jean Chalardimport com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
44fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalardimport com.android.inputmethod.latin.WhitelistDictionary;
453234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.WordComposer;
463234123fba901243990972158d023a5d1c273316Jean Chalard
47db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalardimport java.lang.ref.WeakReference;
486b166a193398554694cb680f704c2ffc23d03a0eJean Chalardimport java.util.ArrayList;
49f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalardimport java.util.Arrays;
503234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Collections;
51cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaokaimport java.util.HashSet;
52db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalardimport java.util.Iterator;
533234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Locale;
543234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Map;
553234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.TreeMap;
563234123fba901243990972158d023a5d1c273316Jean Chalard
57022c1cc20379767966f4915e2dea65fc0b67c0d8satok/**
58022c1cc20379767966f4915e2dea65fc0b67c0d8satok * Service for spell checking, using LatinIME's dictionaries and mechanisms.
59022c1cc20379767966f4915e2dea65fc0b67c0d8satok */
60db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalardpublic class AndroidSpellCheckerService extends SpellCheckerService
61db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        implements SharedPreferences.OnSharedPreferenceChangeListener {
62a90992e56244a914195daba3a2dd8a0e66e63384satok    private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
63a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    private static final boolean DBG = false;
64a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    private static final int POOL_SIZE = 2;
653234123fba901243990972158d023a5d1c273316Jean Chalard
66db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
67db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
68f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
69f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    private static final int CAPITALIZE_FIRST = 1; // First only
70f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    private static final int CAPITALIZE_ALL = 2; // All caps
71f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard
726b166a193398554694cb680f704c2ffc23d03a0eJean Chalard    private final static String[] EMPTY_STRING_ARRAY = new String[0];
73c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard    private Map<String, DictionaryPool> mDictionaryPools =
74a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard            Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
75150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard    private Map<String, Dictionary> mUserDictionaries =
76150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
77fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard    private Map<String, Dictionary> mWhitelistDictionaries =
78fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
7918222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang    private Dictionary mContactsDictionary;
803234123fba901243990972158d023a5d1c273316Jean Chalard
814609c02f9e61370557fee675c67263160fbf7feeJean Chalard    // The threshold for a candidate to be offered as a suggestion.
824609c02f9e61370557fee675c67263160fbf7feeJean Chalard    private double mSuggestionThreshold;
83a409f009fa410019ad10b1134ff57393443eba33Jean Chalard    // The threshold for a suggestion to be considered "recommended".
84a409f009fa410019ad10b1134ff57393443eba33Jean Chalard    private double mRecommendedThreshold;
85db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    // Whether to use the contacts dictionary
86db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private boolean mUseContactsDictionary;
87db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private final Object mUseContactsLock = new Object();
88db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
89db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList =
90db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            new HashSet<WeakReference<DictionaryCollection>>();
9159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
921830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    public static final int SCRIPT_LATIN = 0;
931830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    public static final int SCRIPT_CYRILLIC = 1;
94e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok    private static final String SINGLE_QUOTE = "\u0027";
95e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok    private static final String APOSTROPHE = "\u2019";
961830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    private static final TreeMap<String, Integer> mLanguageToScript;
971830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    static {
981830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        // List of the supported languages and their associated script. We won't check
991830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        // words written in another script than the selected script, because we know we
1001830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        // don't have those in our dictionary so we will underline everything and we
1011830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        // will never have any suggestions, so it makes no sense checking them.
1021830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript = new TreeMap<String, Integer>();
1031830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("en", SCRIPT_LATIN);
1041830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("fr", SCRIPT_LATIN);
1051830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("de", SCRIPT_LATIN);
1061830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("nl", SCRIPT_LATIN);
1071830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("cs", SCRIPT_LATIN);
1081830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("es", SCRIPT_LATIN);
1091830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("it", SCRIPT_LATIN);
1101830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("ru", SCRIPT_CYRILLIC);
1111830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    }
1121830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard
11359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard    @Override public void onCreate() {
11459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        super.onCreate();
1154609c02f9e61370557fee675c67263160fbf7feeJean Chalard        mSuggestionThreshold =
1164609c02f9e61370557fee675c67263160fbf7feeJean Chalard                Double.parseDouble(getString(R.string.spellchecker_suggestion_threshold_value));
117a409f009fa410019ad10b1134ff57393443eba33Jean Chalard        mRecommendedThreshold =
118a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                Double.parseDouble(getString(R.string.spellchecker_recommended_threshold_value));
119db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
120db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        prefs.registerOnSharedPreferenceChangeListener(this);
121db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
122db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    }
123db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
1241830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    private static int getScriptFromLocale(final Locale locale) {
1251830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        final Integer script = mLanguageToScript.get(locale.getLanguage());
1261830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        if (null == script) {
1271830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard            throw new RuntimeException("We have been called with an unsupported language: \""
1281830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard                    + locale.getLanguage() + "\". Framework bug?");
1291830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        }
1301830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        return script;
1311830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    }
1321830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard
133db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    @Override
134db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
135db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
136db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        synchronized(mUseContactsLock) {
137db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            mUseContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true);
138db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (mUseContactsDictionary) {
139db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                startUsingContactsDictionaryLocked();
140db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            } else {
141db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                stopUsingContactsDictionaryLocked();
142db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
143db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        }
144db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    }
145db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
146db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private void startUsingContactsDictionaryLocked() {
147db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        if (null == mContactsDictionary) {
148db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
149db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        }
150db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        final Iterator<WeakReference<DictionaryCollection>> iterator =
151db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                mDictionaryCollectionsList.iterator();
152db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        while (iterator.hasNext()) {
153db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            final WeakReference<DictionaryCollection> dictRef = iterator.next();
154db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            final DictionaryCollection dict = dictRef.get();
155db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (null == dict) {
156db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                iterator.remove();
157db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            } else {
158db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                dict.addDictionary(mContactsDictionary);
159db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
160db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        }
161db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    }
162db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
163db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private void stopUsingContactsDictionaryLocked() {
164db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        if (null == mContactsDictionary) return;
16518222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang        final Dictionary contactsDict = mContactsDictionary;
16618222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang        // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no longer needed
167db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        mContactsDictionary = null;
168db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        final Iterator<WeakReference<DictionaryCollection>> iterator =
169db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                mDictionaryCollectionsList.iterator();
170db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        while (iterator.hasNext()) {
171db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            final WeakReference<DictionaryCollection> dictRef = iterator.next();
172db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            final DictionaryCollection dict = dictRef.get();
173db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (null == dict) {
174db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                iterator.remove();
175db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            } else {
176db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                dict.removeDictionary(contactsDict);
177db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
178db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        }
179db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        contactsDict.close();
18059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard    }
18159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
1825bcf8ee66ceb38675a6b70fefcb574978e0fae92satok    @Override
1835bcf8ee66ceb38675a6b70fefcb574978e0fae92satok    public Session createSession() {
18459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        return new AndroidSpellCheckerSession(this);
1855bcf8ee66ceb38675a6b70fefcb574978e0fae92satok    }
1865bcf8ee66ceb38675a6b70fefcb574978e0fae92satok
187cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard    private static SuggestionsInfo getNotInDictEmptySuggestions() {
188cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard        return new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
189cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard    }
190cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard
191cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard    private static SuggestionsInfo getInDictEmptySuggestions() {
192cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard        return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
193cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard                EMPTY_STRING_ARRAY);
194cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard    }
195cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard
1963234123fba901243990972158d023a5d1c273316Jean Chalard    private static class SuggestionsGatherer implements WordCallback {
19759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        public static class Result {
19859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            public final String[] mSuggestions;
199a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            public final boolean mHasRecommendedSuggestions;
200a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            public Result(final String[] gatheredSuggestions,
201a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                    final boolean hasRecommendedSuggestions) {
20259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                mSuggestions = gatheredSuggestions;
203a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                mHasRecommendedSuggestions = hasRecommendedSuggestions;
20459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            }
20559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        }
20659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
2076b166a193398554694cb680f704c2ffc23d03a0eJean Chalard        private final ArrayList<CharSequence> mSuggestions;
2083234123fba901243990972158d023a5d1c273316Jean Chalard        private final int[] mScores;
20985782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard        private final String mOriginalText;
2104609c02f9e61370557fee675c67263160fbf7feeJean Chalard        private final double mSuggestionThreshold;
211a409f009fa410019ad10b1134ff57393443eba33Jean Chalard        private final double mRecommendedThreshold;
2123234123fba901243990972158d023a5d1c273316Jean Chalard        private final int mMaxLength;
2133234123fba901243990972158d023a5d1c273316Jean Chalard        private int mLength = 0;
21459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
21559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        // The two following attributes are only ever filled if the requested max length
21659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        // is 0 (or less, which is treated the same).
21759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private String mBestSuggestion = null;
21859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private int mBestScore = Integer.MIN_VALUE; // As small as possible
2193234123fba901243990972158d023a5d1c273316Jean Chalard
2204609c02f9e61370557fee675c67263160fbf7feeJean Chalard        SuggestionsGatherer(final String originalText, final double suggestionThreshold,
221a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                final double recommendedThreshold, final int maxLength) {
22285782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard            mOriginalText = originalText;
2234609c02f9e61370557fee675c67263160fbf7feeJean Chalard            mSuggestionThreshold = suggestionThreshold;
224a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            mRecommendedThreshold = recommendedThreshold;
2253234123fba901243990972158d023a5d1c273316Jean Chalard            mMaxLength = maxLength;
2266b166a193398554694cb680f704c2ffc23d03a0eJean Chalard            mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
2273234123fba901243990972158d023a5d1c273316Jean Chalard            mScores = new int[mMaxLength];
2283234123fba901243990972158d023a5d1c273316Jean Chalard        }
2293234123fba901243990972158d023a5d1c273316Jean Chalard
2303234123fba901243990972158d023a5d1c273316Jean Chalard        @Override
2313234123fba901243990972158d023a5d1c273316Jean Chalard        synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
2326e082cb30dbe1a8cc314b474dc1377b85fdb25c2Jean Chalard                int dicTypeId, int dataType) {
233672635493e1dc2baf9fd4a94e73c5b06d0450e7eKen Wakasa            final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score);
2343234123fba901243990972158d023a5d1c273316Jean Chalard            // binarySearch returns the index if the element exists, and -<insertion index> - 1
2353234123fba901243990972158d023a5d1c273316Jean Chalard            // if it doesn't. See documentation for binarySearch.
2363234123fba901243990972158d023a5d1c273316Jean Chalard            final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
2373234123fba901243990972158d023a5d1c273316Jean Chalard
2384609c02f9e61370557fee675c67263160fbf7feeJean Chalard            if (insertIndex == 0 && mLength >= mMaxLength) {
2394609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // In the future, we may want to keep track of the best suggestion score even if
2404609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // we are asked for 0 suggestions. In this case, we can use the following
2414609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // (tested) code to keep it:
2424609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // If the maxLength is 0 (should never be less, but if it is, it's treated as 0)
2434609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // then we need to keep track of the best suggestion in mBestScore and
2444609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // mBestSuggestion. This is so that we know whether the best suggestion makes
2454609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // the score cutoff, since we need to know that to return a meaningful
2464609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // looksLikeTypo.
2474609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // if (0 >= mMaxLength) {
2484609c02f9e61370557fee675c67263160fbf7feeJean Chalard                //     if (score > mBestScore) {
2494609c02f9e61370557fee675c67263160fbf7feeJean Chalard                //         mBestScore = score;
2504609c02f9e61370557fee675c67263160fbf7feeJean Chalard                //         mBestSuggestion = new String(word, wordOffset, wordLength);
2514609c02f9e61370557fee675c67263160fbf7feeJean Chalard                //     }
2524609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // }
2534609c02f9e61370557fee675c67263160fbf7feeJean Chalard                return true;
2544609c02f9e61370557fee675c67263160fbf7feeJean Chalard            }
255c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard            if (insertIndex >= mMaxLength) {
256c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard                // We found a suggestion, but its score is too weak to be kept considering
257c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard                // the suggestion limit.
258c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard                return true;
259c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard            }
2604609c02f9e61370557fee675c67263160fbf7feeJean Chalard
2614609c02f9e61370557fee675c67263160fbf7feeJean Chalard            // Compute the normalized score and skip this word if it's normalized score does not
2624609c02f9e61370557fee675c67263160fbf7feeJean Chalard            // make the threshold.
2634609c02f9e61370557fee675c67263160fbf7feeJean Chalard            final String wordString = new String(word, wordOffset, wordLength);
2644609c02f9e61370557fee675c67263160fbf7feeJean Chalard            final double normalizedScore =
265be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                    BinaryDictionary.calcNormalizedScore(mOriginalText, wordString, score);
2664609c02f9e61370557fee675c67263160fbf7feeJean Chalard            if (normalizedScore < mSuggestionThreshold) {
2674609c02f9e61370557fee675c67263160fbf7feeJean Chalard                if (DBG) Log.i(TAG, wordString + " does not make the score threshold");
2684609c02f9e61370557fee675c67263160fbf7feeJean Chalard                return true;
2694609c02f9e61370557fee675c67263160fbf7feeJean Chalard            }
2704609c02f9e61370557fee675c67263160fbf7feeJean Chalard
2713234123fba901243990972158d023a5d1c273316Jean Chalard            if (mLength < mMaxLength) {
2723234123fba901243990972158d023a5d1c273316Jean Chalard                final int copyLen = mLength - insertIndex;
2733234123fba901243990972158d023a5d1c273316Jean Chalard                ++mLength;
2743234123fba901243990972158d023a5d1c273316Jean Chalard                System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen);
2754609c02f9e61370557fee675c67263160fbf7feeJean Chalard                mSuggestions.add(insertIndex, wordString);
2763234123fba901243990972158d023a5d1c273316Jean Chalard            } else {
2773234123fba901243990972158d023a5d1c273316Jean Chalard                System.arraycopy(mScores, 1, mScores, 0, insertIndex);
2784609c02f9e61370557fee675c67263160fbf7feeJean Chalard                mSuggestions.add(insertIndex, wordString);
2796b166a193398554694cb680f704c2ffc23d03a0eJean Chalard                mSuggestions.remove(0);
2803234123fba901243990972158d023a5d1c273316Jean Chalard            }
2813234123fba901243990972158d023a5d1c273316Jean Chalard            mScores[insertIndex] = score;
2823234123fba901243990972158d023a5d1c273316Jean Chalard
2833234123fba901243990972158d023a5d1c273316Jean Chalard            return true;
2843234123fba901243990972158d023a5d1c273316Jean Chalard        }
2853234123fba901243990972158d023a5d1c273316Jean Chalard
28685782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard        public Result getResults(final int capitalizeType, final Locale locale) {
28759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            final String[] gatheredSuggestions;
288a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            final boolean hasRecommendedSuggestions;
28959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            if (0 == mLength) {
29059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // Either we found no suggestions, or we found some BUT the max length was 0.
29159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // If we found some mBestSuggestion will not be null. If it is null, then
29259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // we found none, regardless of the max length.
29359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                if (null == mBestSuggestion) {
29459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                    gatheredSuggestions = null;
295a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                    hasRecommendedSuggestions = false;
29659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                } else {
29759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                    gatheredSuggestions = EMPTY_STRING_ARRAY;
298be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                    final double normalizedScore = BinaryDictionary.calcNormalizedScore(
299be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                            mOriginalText, mBestSuggestion, mBestScore);
300a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                    hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
30159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                }
30259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            } else {
30359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                if (DBG) {
30459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                    if (mLength != mSuggestions.size()) {
30559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                        Log.e(TAG, "Suggestion size is not the same as stored mLength");
30659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                    }
307af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                    for (int i = mLength - 1; i >= 0; --i) {
308af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                        Log.i(TAG, "" + mScores[i] + " " + mSuggestions.get(i));
309af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                    }
3106b166a193398554694cb680f704c2ffc23d03a0eJean Chalard                }
31159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                Collections.reverse(mSuggestions);
312cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaoka                StringUtils.removeDupes(mSuggestions);
313f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                if (CAPITALIZE_ALL == capitalizeType) {
314f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                    for (int i = 0; i < mSuggestions.size(); ++i) {
315f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                        // get(i) returns a CharSequence which is actually a String so .toString()
316f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                        // should return the same object.
317f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                        mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale));
318f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                    }
319f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                } else if (CAPITALIZE_FIRST == capitalizeType) {
320f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                    for (int i = 0; i < mSuggestions.size(); ++i) {
321f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                        // Likewise
32211d9ee742f8ff3fb31b0e3beb32ee4870c63d8e3Tadashi G. Takaoka                        mSuggestions.set(i, StringUtils.toTitleCase(
3233bf57a5624679a20db26df912077a53b9f90ad36Tadashi G. Takaoka                                mSuggestions.get(i).toString(), locale));
324f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                    }
325f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                }
32659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // This returns a String[], while toArray() returns an Object[] which cannot be cast
32759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // into a String[].
32859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                gatheredSuggestions = mSuggestions.toArray(EMPTY_STRING_ARRAY);
32959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
330af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                final int bestScore = mScores[mLength - 1];
33159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                final CharSequence bestSuggestion = mSuggestions.get(0);
33259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                final double normalizedScore =
333be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                        BinaryDictionary.calcNormalizedScore(
334be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                                mOriginalText, bestSuggestion.toString(), bestScore);
335a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
336af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                if (DBG) {
337af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                    Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore);
3384609c02f9e61370557fee675c67263160fbf7feeJean Chalard                    Log.i(TAG, "Normalized score = " + normalizedScore
339a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                            + " (threshold " + mRecommendedThreshold
340a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                            + ") => hasRecommendedSuggestions = " + hasRecommendedSuggestions);
341af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                }
3423234123fba901243990972158d023a5d1c273316Jean Chalard            }
343a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            return new Result(gatheredSuggestions, hasRecommendedSuggestions);
3443234123fba901243990972158d023a5d1c273316Jean Chalard        }
3453234123fba901243990972158d023a5d1c273316Jean Chalard    }
3463234123fba901243990972158d023a5d1c273316Jean Chalard
347c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard    @Override
348c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard    public boolean onUnbind(final Intent intent) {
3498403611960cd0b2a40b77275c536e8088c098830Jean Chalard        closeAllDictionaries();
3508403611960cd0b2a40b77275c536e8088c098830Jean Chalard        return false;
3518403611960cd0b2a40b77275c536e8088c098830Jean Chalard    }
3528403611960cd0b2a40b77275c536e8088c098830Jean Chalard
3538403611960cd0b2a40b77275c536e8088c098830Jean Chalard    private void closeAllDictionaries() {
354c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard        final Map<String, DictionaryPool> oldPools = mDictionaryPools;
355c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
356150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
357150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
358fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
359fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
360c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard        for (DictionaryPool pool : oldPools.values()) {
361c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard            pool.close();
362c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard        }
363150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        for (Dictionary dict : oldUserDictionaries.values()) {
364150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard            dict.close();
365150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        }
366fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        for (Dictionary dict : oldWhitelistDictionaries.values()) {
367fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            dict.close();
368fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        }
36918222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang        synchronized (mUseContactsLock) {
370db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (null != mContactsDictionary) {
371db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                // The synchronously loaded contacts dictionary should have been in one
372db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                // or several pools, but it is shielded against multiple closing and it's
373db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                // safe to call it several times.
37418222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                final Dictionary dictToClose = mContactsDictionary;
37518222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no
37618222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                // longer needed
377db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                mContactsDictionary = null;
378db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                dictToClose.close();
379db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
3802e3c6da8688a907024d4d8e0f2db3e0ed4fab8dbJean Chalard        }
381c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard    }
382c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard
383a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    private DictionaryPool getDictionaryPool(final String locale) {
384a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard        DictionaryPool pool = mDictionaryPools.get(locale);
385a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard        if (null == pool) {
386ef35cb631c45c8b106fe7ed9e0d1178c3e5fb963Jean Chalard            final Locale localeObject = LocaleUtils.constructLocaleFromString(locale);
387a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard            pool = new DictionaryPool(POOL_SIZE, this, localeObject);
388a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard            mDictionaryPools.put(locale, pool);
3893234123fba901243990972158d023a5d1c273316Jean Chalard        }
390a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard        return pool;
391a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    }
392a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard
393a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    public DictAndProximity createDictAndProximity(final Locale locale) {
3941830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        final int script = getScriptFromLocale(locale);
3951830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo(
39688794b24c0928e3bbea59999fce47c78c028863dKen Wakasa                SpellCheckerProximityInfo.getProximityForScript(script),
39788794b24c0928e3bbea59999fce47c78c028863dKen Wakasa                SpellCheckerProximityInfo.ROW_SIZE,
39888794b24c0928e3bbea59999fce47c78c028863dKen Wakasa                SpellCheckerProximityInfo.PROXIMITY_GRID_WIDTH,
39988794b24c0928e3bbea59999fce47c78c028863dKen Wakasa                SpellCheckerProximityInfo.PROXIMITY_GRID_HEIGHT);
400150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        final DictionaryCollection dictionaryCollection =
401f0e12a969974987f1b97929886c6ebe6a685c538Jean Chalard                DictionaryFactory.createMainDictionaryFromManager(this, locale,
40224aee9100e92dc4c06cdb54487a4922420fa8660Jean Chalard                        true /* useFullEditDistance */);
403150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        final String localeStr = locale.toString();
404fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        Dictionary userDictionary = mUserDictionaries.get(localeStr);
405fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        if (null == userDictionary) {
406fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
407fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            mUserDictionaries.put(localeStr, userDictionary);
408fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        }
409fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        dictionaryCollection.addDictionary(userDictionary);
410fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr);
411fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        if (null == whitelistDictionary) {
412fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            whitelistDictionary = new WhitelistDictionary(this, locale);
413fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            mWhitelistDictionaries.put(localeStr, whitelistDictionary);
414150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        }
415fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        dictionaryCollection.addDictionary(whitelistDictionary);
41618222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang        synchronized (mUseContactsLock) {
417db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (mUseContactsDictionary) {
418db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                if (null == mContactsDictionary) {
41918222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no
42018222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    // longer needed
42118222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
42218222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                        mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this);
42318222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    } else {
42418222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                        mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
42518222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    }
426db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                }
427db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
428db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            dictionaryCollection.addDictionary(mContactsDictionary);
429db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            mDictionaryCollectionsList.add(
430db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                    new WeakReference<DictionaryCollection>(dictionaryCollection));
4312e3c6da8688a907024d4d8e0f2db3e0ed4fab8dbJean Chalard        }
432150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        return new DictAndProximity(dictionaryCollection, proximityInfo);
4333234123fba901243990972158d023a5d1c273316Jean Chalard    }
4343234123fba901243990972158d023a5d1c273316Jean Chalard
435f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    // This method assumes the text is not empty or null.
436f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    private static int getCapitalizationType(String text) {
437f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // If the first char is not uppercase, then the word is either all lower case,
438f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // and in either case we return CAPITALIZE_NONE.
439f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE;
4409242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard        final int len = text.length();
441f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        int capsCount = 1;
4429242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard        for (int i = 1; i < len; i = text.offsetByCodePoints(i, 1)) {
443f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard            if (1 != capsCount && i != capsCount) break;
444f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard            if (Character.isUpperCase(text.codePointAt(i))) ++capsCount;
445f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        }
446f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // We know the first char is upper case. So we want to test if either everything
447f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // else is lower case, or if everything else is upper case. If the string is
448f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // exactly one char long, then we will arrive here with capsCount 1, and this is
449f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // correct, too.
450f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        if (1 == capsCount) return CAPITALIZE_FIRST;
451f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
452f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    }
453f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard
45459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard    private static class AndroidSpellCheckerSession extends Session {
455a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard        // Immutable, but need the locale which is not available in the constructor yet
45659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private DictionaryPool mDictionaryPool;
4575d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard        // Likewise
45859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private Locale mLocale;
459bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard        // Cache this for performance
460bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard        private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
46159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
46259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private final AndroidSpellCheckerService mService;
46359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
46474a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        private final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
46574a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
46674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        private static class SuggestionsParams {
46774a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public final String[] mSuggestions;
46874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public final int mFlags;
46974a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public SuggestionsParams(String[] suggestions, int flags) {
47074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                mSuggestions = suggestions;
47174a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                mFlags = flags;
47274a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            }
47374a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        }
47474a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
47574a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        private static class SuggestionsCache {
47674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            private static final int MAX_CACHE_SIZE = 50;
47774a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            // TODO: support bigram
47874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
47974a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
48074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
48174a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public SuggestionsParams getSuggestionsFromCache(String query) {
48274a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                return mUnigramSuggestionsInfoCache.get(query);
48374a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            }
48474a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
48574a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public void putSuggestionsToCache(String query, String[] suggestions, int flags) {
48674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                if (suggestions == null || TextUtils.isEmpty(query)) {
48774a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    return;
48874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                }
48974a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                mUnigramSuggestionsInfoCache.put(query, new SuggestionsParams(suggestions, flags));
49074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            }
4915434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok
4925434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            public void remove(String key) {
4935434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                mUnigramSuggestionsInfoCache.remove(key);
4945434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
49574a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        }
49674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
49759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        AndroidSpellCheckerSession(final AndroidSpellCheckerService service) {
49859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            mService = service;
49959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        }
500a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard
5015bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        @Override
5025bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        public void onCreate() {
5035d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard            final String localeString = getLocale();
50459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            mDictionaryPool = mService.getDictionaryPool(localeString);
505ef35cb631c45c8b106fe7ed9e0d1178c3e5fb963Jean Chalard            mLocale = LocaleUtils.constructLocaleFromString(localeString);
5061830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard            mScript = getScriptFromLocale(mLocale);
507a90992e56244a914195daba3a2dd8a0e66e63384satok        }
5083234123fba901243990972158d023a5d1c273316Jean Chalard
50972479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard        /*
51072479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard         * Returns whether the code point is a letter that makes sense for the specified
51172479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard         * locale for this spell checker.
51272479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard         * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
513bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * and is limited to EFIGS languages and Russian.
514bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
515bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
51672479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard         */
51772479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard        private static boolean isLetterCheckableByLanguage(final int codePoint,
518bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                final int script) {
519bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            switch (script) {
520bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            case SCRIPT_LATIN:
521bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // Our supported latin script dictionaries (EFIGS) at the moment only include
522bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
523bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
524bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
525bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // excluded from isLetter anyway.
526bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                return codePoint <= 0x2AF && Character.isLetter(codePoint);
527bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            case SCRIPT_CYRILLIC:
528bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // All Cyrillic characters are in the 400~52F block. There are some in the upper
529bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // Unicode range, but they are archaic characters that are not used in modern
530bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // russian and are not used by our dictionary.
531bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
532bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            default:
533bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // Should never come here
534bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                throw new RuntimeException("Impossible value of script: " + script);
535bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            }
53672479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard        }
53772479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard
53888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard        /**
53988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         * Finds out whether a particular string should be filtered out of spell checking.
54088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         *
541bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
542bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * we know we will never recognize, this accepts a script identifier that should be one
543bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * of the SCRIPT_* constants defined above, to rule out quickly characters from very
544bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * different languages.
54588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         *
54688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         * @param text the string to evaluate.
547bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * @param script the identifier for the script this spell checker recognizes
54888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         * @return true if we should filter this text out, false otherwise
54988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         */
550bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard        private static boolean shouldFilterOut(final String text, final int script) {
55188fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
55288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard
55388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // TODO: check if an equivalent processing can't be done more quickly with a
55488fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // compiled regexp.
55588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // Filter by first letter
55688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            final int firstCodePoint = text.codePointAt(0);
55788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // Filter out words that don't start with a letter or an apostrophe
558bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            if (!isLetterCheckableByLanguage(firstCodePoint, script)
55988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                    && '\'' != firstCodePoint) return true;
56088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard
56188fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // Filter contents
56288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            final int length = text.length();
56388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            int letterCount = 0;
5649242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard            for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
56588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                final int codePoint = text.codePointAt(i);
56688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                // Any word containing a '@' is probably an e-mail address
56788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                // Any word containing a '/' is probably either an ad-hoc combination of two
56888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                // words or a URI - in either case we don't want to spell check that
5699242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard                if ('@' == codePoint || '/' == codePoint) return true;
570bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
57188fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            }
57288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
57388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // in this word are letters
57488fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            return (letterCount * 4 < length * 3);
57588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard        }
57688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard
5775434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(
5785434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                TextInfo ti, SentenceSuggestionsInfo ssi) {
5795434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final String typedText = ti.getText();
5805434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            if (!typedText.contains(SINGLE_QUOTE)) {
5815434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                return null;
5825434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
5835434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int N = ssi.getSuggestionsCount();
5845434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final ArrayList<Integer> additionalOffsets = new ArrayList<Integer>();
5855434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final ArrayList<Integer> additionalLengths = new ArrayList<Integer>();
5865434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
5875434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    new ArrayList<SuggestionsInfo>();
5885434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            for (int i = 0; i < N; ++i) {
5895434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
5905434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final int flags = si.getSuggestionsAttributes();
5915434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
5925434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    continue;
5935434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
5945434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final int offset = ssi.getOffsetAt(i);
5955434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final int length = ssi.getLengthAt(i);
5965434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final String subText = typedText.substring(offset, offset + length);
5975434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                if (!subText.contains(SINGLE_QUOTE)) {
5985434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    continue;
5995434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6005434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final String[] splitTexts = subText.split(SINGLE_QUOTE, -1);
6015434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                if (splitTexts == null || splitTexts.length <= 1) {
6025434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    continue;
6035434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6045434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final int splitNum = splitTexts.length;
6055434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                for (int j = 0; j < splitNum; ++j) {
6065434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    final String splitText = splitTexts[j];
6075434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    if (TextUtils.isEmpty(splitText)) {
6085434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                        continue;
6095434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    }
6105434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    if (mSuggestionsCache.getSuggestionsFromCache(splitText) == null) {
6115434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                        continue;
6125434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    }
6135434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    final int newLength = splitText.length();
6145434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
6155434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    final int newFlags = 0;
6165434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
6175434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
6185434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    if (DBG) {
6195434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                        Log.d(TAG, "Override and remove old span over: "
6205434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                                + splitText + ", " + offset + "," + newLength);
6215434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    }
6225434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    additionalOffsets.add(offset);
6235434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    additionalLengths.add(newLength);
6245434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    additionalSuggestionsInfos.add(newSi);
6255434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6265434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6275434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int additionalSize = additionalOffsets.size();
6285434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            if (additionalSize <= 0) {
6295434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                return null;
6305434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6315434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int suggestionsSize = N + additionalSize;
6325434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int[] newOffsets = new int[suggestionsSize];
6335434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int[] newLengths = new int[suggestionsSize];
6345434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
6355434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            int i;
6365434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            for (i = 0; i < N; ++i) {
6375434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newOffsets[i] = ssi.getOffsetAt(i);
6385434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newLengths[i] = ssi.getLengthAt(i);
6395434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
6405434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6415434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            for (; i < suggestionsSize; ++i) {
6425434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newOffsets[i] = additionalOffsets.get(i - N);
6435434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newLengths[i] = additionalLengths.get(i - N);
6445434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
6455434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6465434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
6475434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        }
6485434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok
6495434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        @Override
6505434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(
6515434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                TextInfo[] textInfos, int suggestionsLimit) {
6525434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final SentenceSuggestionsInfo[] retval = super.onGetSentenceSuggestionsMultiple(
6535434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    textInfos, suggestionsLimit);
6545434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            if (retval == null || retval.length != textInfos.length) {
6555434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                return retval;
6565434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6575434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            for (int i = 0; i < retval.length; ++i) {
6585434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final SentenceSuggestionsInfo tempSsi =
6595434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                        fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
6605434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                if (tempSsi != null) {
6615434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    retval[i] = tempSsi;
6625434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6635434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6645434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            return retval;
6655434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        }
6665434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok
6675bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        // Note : this must be reentrant
6685bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        /**
6695bcf8ee66ceb38675a6b70fefcb574978e0fae92satok         * Gets a list of suggestions for a specific string. This returns a list of possible
67070b9c5d9913b676f21fe29f795bdb25324509205Jean Chalard         * corrections for the text passed as an argument. It may split or group words, and
6715bcf8ee66ceb38675a6b70fefcb574978e0fae92satok         * even perform grammatical analysis.
6725bcf8ee66ceb38675a6b70fefcb574978e0fae92satok         */
6735bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        @Override
6745bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
6755bcf8ee66ceb38675a6b70fefcb574978e0fae92satok                final int suggestionsLimit) {
676a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard            try {
677e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                final String inText = textInfo.getText();
67874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                final SuggestionsParams cachedSuggestionsParams =
679e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                        mSuggestionsCache.getSuggestionsFromCache(inText);
68074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                if (cachedSuggestionsParams != null) {
68174a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    if (DBG) {
682e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                        Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
68374a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    }
68474a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    return new SuggestionsInfo(
68574a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                            cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
68674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                }
687199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard
688e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                if (shouldFilterOut(inText, mScript)) {
689a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    DictAndProximity dictInfo = null;
690a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    try {
691a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        dictInfo = mDictionaryPool.takeOrGetNull();
692cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard                        if (null == dictInfo) return getNotInDictEmptySuggestions();
693e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                        return dictInfo.mDictionary.isValidWord(inText) ?
694e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                                getInDictEmptySuggestions() : getNotInDictEmptySuggestions();
695a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    } finally {
696a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        if (null != dictInfo) {
697a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                            if (!mDictionaryPool.offer(dictInfo)) {
698a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                                Log.e(TAG, "Can't re-insert a dictionary into its pool");
699a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                            }
700a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        }
701a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    }
702e897e4d3422c8d9d8b6f051376cc2ba16e4d5945Jean Chalard                }
703e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                final String text = inText.replaceAll(APOSTROPHE, SINGLE_QUOTE);
704199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard
705647db70fec321d9847f6568cc7bd2b3bd6671322Jean Chalard                // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
7064609c02f9e61370557fee675c67263160fbf7feeJean Chalard                final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
707a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                        mService.mSuggestionThreshold, mService.mRecommendedThreshold,
708a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                        suggestionsLimit);
709199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                final WordComposer composer = new WordComposer();
710199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                final int length = text.length();
7119242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard                for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
712081616cd2f472295449268cecb570771b969cba3Jean Chalard                    final int codePoint = text.codePointAt(i);
713081616cd2f472295449268cecb570771b969cba3Jean Chalard                    // The getXYForCodePointAndScript method returns (Y << 16) + X
714081616cd2f472295449268cecb570771b969cba3Jean Chalard                    final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
715081616cd2f472295449268cecb570771b969cba3Jean Chalard                            codePoint, mScript);
716b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                    if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
717b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                        composer.add(codePoint, WordComposer.NOT_A_COORDINATE,
718b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                                WordComposer.NOT_A_COORDINATE, null);
719b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                    } else {
720b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                        composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
721b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                    }
7225d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard                }
723199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard
724199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                final int capitalizeType = getCapitalizationType(text);
725199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                boolean isInDict = true;
726a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                DictAndProximity dictInfo = null;
727a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                try {
728a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    dictInfo = mDictionaryPool.takeOrGetNull();
729cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard                    if (null == dictInfo) return getNotInDictEmptySuggestions();
730ac27e4544b5b5ff7b4f365a4bde5c288d511ae13Jean Chalard                    dictInfo.mDictionary.getWords(composer, null, suggestionsGatherer,
731a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                            dictInfo.mProximityInfo);
732a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    isInDict = dictInfo.mDictionary.isValidWord(text);
733a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    if (!isInDict && CAPITALIZE_NONE != capitalizeType) {
734a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        // We want to test the word again if it's all caps or first caps only.
735a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        // If it's fully down, we already tested it, if it's mixed case, we don't
736a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        // want to test a lowercase version of it.
737a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
738a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    }
739a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                } finally {
740a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    if (null != dictInfo) {
741a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        if (!mDictionaryPool.offer(dictInfo)) {
742a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                            Log.e(TAG, "Can't re-insert a dictionary into its pool");
743a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        }
744a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    }
745c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard                }
7465bcf8ee66ceb38675a6b70fefcb574978e0fae92satok
74785782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard                final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
74885782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard                        capitalizeType, mLocale);
749199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard
750199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                if (DBG) {
751199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                    Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
752199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                            + suggestionsLimit);
753647db70fec321d9847f6568cc7bd2b3bd6671322Jean Chalard                    Log.i(TAG, "IsInDict = " + isInDict);
754647db70fec321d9847f6568cc7bd2b3bd6671322Jean Chalard                    Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
755a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                    Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
75651075d145a85d1acaff08c02f4d6b10b175eaa36Jean Chalard                    if (null != result.mSuggestions) {
75751075d145a85d1acaff08c02f4d6b10b175eaa36Jean Chalard                        for (String suggestion : result.mSuggestions) {
75851075d145a85d1acaff08c02f4d6b10b175eaa36Jean Chalard                            Log.i(TAG, suggestion);
75951075d145a85d1acaff08c02f4d6b10b175eaa36Jean Chalard                        }
760199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                    }
761199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                }
762a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard
763199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                final int flags =
764647db70fec321d9847f6568cc7bd2b3bd6671322Jean Chalard                        (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
765a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                                : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
766a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                        | (result.mHasRecommendedSuggestions
7679260422423819ed6942f11c03960d5764e97c262Ken Wakasa                                ? SuggestionsInfoCompatUtils
7689260422423819ed6942f11c03960d5764e97c262Ken Wakasa                                        .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
769a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                                : 0);
77074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
77174a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags);
77274a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                return retval;
773199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard            } catch (RuntimeException e) {
774199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                // Don't kill the keyboard if there is a bug in the spell checker
775199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                if (DBG) {
776199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                    throw e;
777199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                } else {
778199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                    Log.e(TAG, "Exception while spellcheking: " + e);
779cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard                    return getNotInDictEmptySuggestions();
780af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                }
781af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard            }
7825bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        }
783022c1cc20379767966f4915e2dea65fc0b67c0d8satok    }
784022c1cc20379767966f4915e2dea65fc0b67c0d8satok}
785