AndroidSpellCheckerService.java revision d527a15ec44089930dd23c9e20b8672024a4555b
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;
43f6adff6227a15af105dbf39c57213a24bf16780bTom Ouyangimport com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary;
44f019d505d7da97c03c321eef02c4879c4e0448f6Jean Chalardimport com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
45fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalardimport com.android.inputmethod.latin.WhitelistDictionary;
463234123fba901243990972158d023a5d1c273316Jean Chalardimport com.android.inputmethod.latin.WordComposer;
473234123fba901243990972158d023a5d1c273316Jean Chalard
48db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalardimport java.lang.ref.WeakReference;
496b166a193398554694cb680f704c2ffc23d03a0eJean Chalardimport java.util.ArrayList;
50f098fbbef324df034cc04de04d9b5fe6657238c7Jean Chalardimport java.util.Arrays;
513234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Collections;
52cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaokaimport java.util.HashSet;
53db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalardimport java.util.Iterator;
543234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Locale;
553234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.Map;
563234123fba901243990972158d023a5d1c273316Jean Chalardimport java.util.TreeMap;
573234123fba901243990972158d023a5d1c273316Jean Chalard
58022c1cc20379767966f4915e2dea65fc0b67c0d8satok/**
59022c1cc20379767966f4915e2dea65fc0b67c0d8satok * Service for spell checking, using LatinIME's dictionaries and mechanisms.
60022c1cc20379767966f4915e2dea65fc0b67c0d8satok */
61db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalardpublic class AndroidSpellCheckerService extends SpellCheckerService
62db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        implements SharedPreferences.OnSharedPreferenceChangeListener {
63a90992e56244a914195daba3a2dd8a0e66e63384satok    private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
64a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    private static final boolean DBG = false;
65a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    private static final int POOL_SIZE = 2;
663234123fba901243990972158d023a5d1c273316Jean Chalard
67db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
68db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
69f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
70f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    private static final int CAPITALIZE_FIRST = 1; // First only
71f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    private static final int CAPITALIZE_ALL = 2; // All caps
72f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard
736b166a193398554694cb680f704c2ffc23d03a0eJean Chalard    private final static String[] EMPTY_STRING_ARRAY = new String[0];
74c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard    private Map<String, DictionaryPool> mDictionaryPools =
75a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard            Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
76150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard    private Map<String, Dictionary> mUserDictionaries =
77150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
78fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard    private Map<String, Dictionary> mWhitelistDictionaries =
79fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
8018222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang    private Dictionary mContactsDictionary;
813234123fba901243990972158d023a5d1c273316Jean Chalard
824609c02f9e61370557fee675c67263160fbf7feeJean Chalard    // The threshold for a candidate to be offered as a suggestion.
830028ed3627ff4f37a62a80f3b2c857e373cd5090satok    private float mSuggestionThreshold;
84a409f009fa410019ad10b1134ff57393443eba33Jean Chalard    // The threshold for a suggestion to be considered "recommended".
850028ed3627ff4f37a62a80f3b2c857e373cd5090satok    private float mRecommendedThreshold;
86db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    // Whether to use the contacts dictionary
87db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private boolean mUseContactsDictionary;
88db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private final Object mUseContactsLock = new Object();
89db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
90db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList =
91db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            new HashSet<WeakReference<DictionaryCollection>>();
9259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
931830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    public static final int SCRIPT_LATIN = 0;
941830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    public static final int SCRIPT_CYRILLIC = 1;
95e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok    private static final String SINGLE_QUOTE = "\u0027";
96e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok    private static final String APOSTROPHE = "\u2019";
971830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    private static final TreeMap<String, Integer> mLanguageToScript;
981830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    static {
991830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        // List of the supported languages and their associated script. We won't check
1001830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        // words written in another script than the selected script, because we know we
1011830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        // don't have those in our dictionary so we will underline everything and we
1021830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        // will never have any suggestions, so it makes no sense checking them.
1031830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript = new TreeMap<String, Integer>();
1041830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("en", SCRIPT_LATIN);
105d527a15ec44089930dd23c9e20b8672024a4555bJean Chalard        mLanguageToScript.put("en_US", SCRIPT_LATIN);
106d527a15ec44089930dd23c9e20b8672024a4555bJean Chalard        mLanguageToScript.put("en_GB", SCRIPT_LATIN);
1071830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("fr", SCRIPT_LATIN);
1081830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("de", SCRIPT_LATIN);
1091830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("nl", SCRIPT_LATIN);
1101830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("cs", SCRIPT_LATIN);
1111830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("es", SCRIPT_LATIN);
1121830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("it", SCRIPT_LATIN);
113d527a15ec44089930dd23c9e20b8672024a4555bJean Chalard        mLanguageToScript.put("hr", SCRIPT_LATIN);
114d527a15ec44089930dd23c9e20b8672024a4555bJean Chalard        mLanguageToScript.put("pt_BR", SCRIPT_LATIN);
1151830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        mLanguageToScript.put("ru", SCRIPT_CYRILLIC);
116d527a15ec44089930dd23c9e20b8672024a4555bJean Chalard        // TODO: Make a persian proximity, and activate the Farsi subtype.
117d527a15ec44089930dd23c9e20b8672024a4555bJean Chalard        // mLanguageToScript.put("fa", SCRIPT_PERSIAN);
1181830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    }
1191830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard
12059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard    @Override public void onCreate() {
12159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        super.onCreate();
1224609c02f9e61370557fee675c67263160fbf7feeJean Chalard        mSuggestionThreshold =
1230028ed3627ff4f37a62a80f3b2c857e373cd5090satok                Float.parseFloat(getString(R.string.spellchecker_suggestion_threshold_value));
124a409f009fa410019ad10b1134ff57393443eba33Jean Chalard        mRecommendedThreshold =
1250028ed3627ff4f37a62a80f3b2c857e373cd5090satok                Float.parseFloat(getString(R.string.spellchecker_recommended_threshold_value));
126db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
127db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        prefs.registerOnSharedPreferenceChangeListener(this);
128db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
129db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    }
130db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
1311830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    private static int getScriptFromLocale(final Locale locale) {
1321830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        final Integer script = mLanguageToScript.get(locale.getLanguage());
1331830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        if (null == script) {
1341830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard            throw new RuntimeException("We have been called with an unsupported language: \""
1351830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard                    + locale.getLanguage() + "\". Framework bug?");
1361830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        }
1371830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        return script;
1381830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard    }
1391830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard
140db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    @Override
141db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
142db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
143db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        synchronized(mUseContactsLock) {
144db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            mUseContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true);
145db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (mUseContactsDictionary) {
146db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                startUsingContactsDictionaryLocked();
147db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            } else {
148db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                stopUsingContactsDictionaryLocked();
149db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
150db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        }
151db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    }
152db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
153db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private void startUsingContactsDictionaryLocked() {
154db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        if (null == mContactsDictionary) {
155db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
156db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        }
157db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        final Iterator<WeakReference<DictionaryCollection>> iterator =
158db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                mDictionaryCollectionsList.iterator();
159db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        while (iterator.hasNext()) {
160db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            final WeakReference<DictionaryCollection> dictRef = iterator.next();
161db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            final DictionaryCollection dict = dictRef.get();
162db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (null == dict) {
163db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                iterator.remove();
164db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            } else {
165db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                dict.addDictionary(mContactsDictionary);
166db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
167db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        }
168db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    }
169db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard
170db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard    private void stopUsingContactsDictionaryLocked() {
171db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        if (null == mContactsDictionary) return;
17218222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang        final Dictionary contactsDict = mContactsDictionary;
17318222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang        // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no longer needed
174db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        mContactsDictionary = null;
175db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        final Iterator<WeakReference<DictionaryCollection>> iterator =
176db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                mDictionaryCollectionsList.iterator();
177db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        while (iterator.hasNext()) {
178db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            final WeakReference<DictionaryCollection> dictRef = iterator.next();
179db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            final DictionaryCollection dict = dictRef.get();
180db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (null == dict) {
181db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                iterator.remove();
182db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            } else {
183db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                dict.removeDictionary(contactsDict);
184db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
185db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        }
186db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard        contactsDict.close();
18759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard    }
18859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
1895bcf8ee66ceb38675a6b70fefcb574978e0fae92satok    @Override
1905bcf8ee66ceb38675a6b70fefcb574978e0fae92satok    public Session createSession() {
19159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        return new AndroidSpellCheckerSession(this);
1925bcf8ee66ceb38675a6b70fefcb574978e0fae92satok    }
1935bcf8ee66ceb38675a6b70fefcb574978e0fae92satok
194cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard    private static SuggestionsInfo getNotInDictEmptySuggestions() {
195cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard        return new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
196cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard    }
197cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard
198cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard    private static SuggestionsInfo getInDictEmptySuggestions() {
199cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard        return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
200cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard                EMPTY_STRING_ARRAY);
201cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard    }
202cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard
2033234123fba901243990972158d023a5d1c273316Jean Chalard    private static class SuggestionsGatherer implements WordCallback {
20459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        public static class Result {
20559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            public final String[] mSuggestions;
206a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            public final boolean mHasRecommendedSuggestions;
207a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            public Result(final String[] gatheredSuggestions,
208a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                    final boolean hasRecommendedSuggestions) {
20959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                mSuggestions = gatheredSuggestions;
210a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                mHasRecommendedSuggestions = hasRecommendedSuggestions;
21159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            }
21259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        }
21359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
2146b166a193398554694cb680f704c2ffc23d03a0eJean Chalard        private final ArrayList<CharSequence> mSuggestions;
2153234123fba901243990972158d023a5d1c273316Jean Chalard        private final int[] mScores;
21685782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard        private final String mOriginalText;
2170028ed3627ff4f37a62a80f3b2c857e373cd5090satok        private final float mSuggestionThreshold;
2180028ed3627ff4f37a62a80f3b2c857e373cd5090satok        private final float mRecommendedThreshold;
2193234123fba901243990972158d023a5d1c273316Jean Chalard        private final int mMaxLength;
2203234123fba901243990972158d023a5d1c273316Jean Chalard        private int mLength = 0;
22159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
22259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        // The two following attributes are only ever filled if the requested max length
22359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        // is 0 (or less, which is treated the same).
22459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private String mBestSuggestion = null;
22559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private int mBestScore = Integer.MIN_VALUE; // As small as possible
2263234123fba901243990972158d023a5d1c273316Jean Chalard
2270028ed3627ff4f37a62a80f3b2c857e373cd5090satok        SuggestionsGatherer(final String originalText, final float suggestionThreshold,
2280028ed3627ff4f37a62a80f3b2c857e373cd5090satok                final float recommendedThreshold, final int maxLength) {
22985782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard            mOriginalText = originalText;
2304609c02f9e61370557fee675c67263160fbf7feeJean Chalard            mSuggestionThreshold = suggestionThreshold;
231a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            mRecommendedThreshold = recommendedThreshold;
2323234123fba901243990972158d023a5d1c273316Jean Chalard            mMaxLength = maxLength;
2336b166a193398554694cb680f704c2ffc23d03a0eJean Chalard            mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
2343234123fba901243990972158d023a5d1c273316Jean Chalard            mScores = new int[mMaxLength];
2353234123fba901243990972158d023a5d1c273316Jean Chalard        }
2363234123fba901243990972158d023a5d1c273316Jean Chalard
2373234123fba901243990972158d023a5d1c273316Jean Chalard        @Override
2383234123fba901243990972158d023a5d1c273316Jean Chalard        synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
2396e082cb30dbe1a8cc314b474dc1377b85fdb25c2Jean Chalard                int dicTypeId, int dataType) {
240672635493e1dc2baf9fd4a94e73c5b06d0450e7eKen Wakasa            final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score);
2413234123fba901243990972158d023a5d1c273316Jean Chalard            // binarySearch returns the index if the element exists, and -<insertion index> - 1
2423234123fba901243990972158d023a5d1c273316Jean Chalard            // if it doesn't. See documentation for binarySearch.
2433234123fba901243990972158d023a5d1c273316Jean Chalard            final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
2443234123fba901243990972158d023a5d1c273316Jean Chalard
2454609c02f9e61370557fee675c67263160fbf7feeJean Chalard            if (insertIndex == 0 && mLength >= mMaxLength) {
2464609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // In the future, we may want to keep track of the best suggestion score even if
2474609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // we are asked for 0 suggestions. In this case, we can use the following
2484609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // (tested) code to keep it:
2494609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // If the maxLength is 0 (should never be less, but if it is, it's treated as 0)
2504609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // then we need to keep track of the best suggestion in mBestScore and
2514609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // mBestSuggestion. This is so that we know whether the best suggestion makes
2524609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // the score cutoff, since we need to know that to return a meaningful
2534609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // looksLikeTypo.
2544609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // if (0 >= mMaxLength) {
2554609c02f9e61370557fee675c67263160fbf7feeJean Chalard                //     if (score > mBestScore) {
2564609c02f9e61370557fee675c67263160fbf7feeJean Chalard                //         mBestScore = score;
2574609c02f9e61370557fee675c67263160fbf7feeJean Chalard                //         mBestSuggestion = new String(word, wordOffset, wordLength);
2584609c02f9e61370557fee675c67263160fbf7feeJean Chalard                //     }
2594609c02f9e61370557fee675c67263160fbf7feeJean Chalard                // }
2604609c02f9e61370557fee675c67263160fbf7feeJean Chalard                return true;
2614609c02f9e61370557fee675c67263160fbf7feeJean Chalard            }
262c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard            if (insertIndex >= mMaxLength) {
263c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard                // We found a suggestion, but its score is too weak to be kept considering
264c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard                // the suggestion limit.
265c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard                return true;
266c53661f152f2d676f8cec656cbdd93adfa7fc908Jean Chalard            }
2674609c02f9e61370557fee675c67263160fbf7feeJean Chalard
2684609c02f9e61370557fee675c67263160fbf7feeJean Chalard            // Compute the normalized score and skip this word if it's normalized score does not
2694609c02f9e61370557fee675c67263160fbf7feeJean Chalard            // make the threshold.
2704609c02f9e61370557fee675c67263160fbf7feeJean Chalard            final String wordString = new String(word, wordOffset, wordLength);
2710028ed3627ff4f37a62a80f3b2c857e373cd5090satok            final float normalizedScore =
272be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                    BinaryDictionary.calcNormalizedScore(mOriginalText, wordString, score);
2734609c02f9e61370557fee675c67263160fbf7feeJean Chalard            if (normalizedScore < mSuggestionThreshold) {
2744609c02f9e61370557fee675c67263160fbf7feeJean Chalard                if (DBG) Log.i(TAG, wordString + " does not make the score threshold");
2754609c02f9e61370557fee675c67263160fbf7feeJean Chalard                return true;
2764609c02f9e61370557fee675c67263160fbf7feeJean Chalard            }
2774609c02f9e61370557fee675c67263160fbf7feeJean Chalard
2783234123fba901243990972158d023a5d1c273316Jean Chalard            if (mLength < mMaxLength) {
2793234123fba901243990972158d023a5d1c273316Jean Chalard                final int copyLen = mLength - insertIndex;
2803234123fba901243990972158d023a5d1c273316Jean Chalard                ++mLength;
2813234123fba901243990972158d023a5d1c273316Jean Chalard                System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen);
2824609c02f9e61370557fee675c67263160fbf7feeJean Chalard                mSuggestions.add(insertIndex, wordString);
2833234123fba901243990972158d023a5d1c273316Jean Chalard            } else {
2843234123fba901243990972158d023a5d1c273316Jean Chalard                System.arraycopy(mScores, 1, mScores, 0, insertIndex);
2854609c02f9e61370557fee675c67263160fbf7feeJean Chalard                mSuggestions.add(insertIndex, wordString);
2866b166a193398554694cb680f704c2ffc23d03a0eJean Chalard                mSuggestions.remove(0);
2873234123fba901243990972158d023a5d1c273316Jean Chalard            }
2883234123fba901243990972158d023a5d1c273316Jean Chalard            mScores[insertIndex] = score;
2893234123fba901243990972158d023a5d1c273316Jean Chalard
2903234123fba901243990972158d023a5d1c273316Jean Chalard            return true;
2913234123fba901243990972158d023a5d1c273316Jean Chalard        }
2923234123fba901243990972158d023a5d1c273316Jean Chalard
29385782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard        public Result getResults(final int capitalizeType, final Locale locale) {
29459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            final String[] gatheredSuggestions;
295a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            final boolean hasRecommendedSuggestions;
29659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            if (0 == mLength) {
29759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // Either we found no suggestions, or we found some BUT the max length was 0.
29859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // If we found some mBestSuggestion will not be null. If it is null, then
29959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // we found none, regardless of the max length.
30059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                if (null == mBestSuggestion) {
30159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                    gatheredSuggestions = null;
302a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                    hasRecommendedSuggestions = false;
30359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                } else {
30459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                    gatheredSuggestions = EMPTY_STRING_ARRAY;
3050028ed3627ff4f37a62a80f3b2c857e373cd5090satok                    final float normalizedScore = BinaryDictionary.calcNormalizedScore(
306be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                            mOriginalText, mBestSuggestion, mBestScore);
307a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                    hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
30859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                }
30959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            } else {
31059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                if (DBG) {
31159b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                    if (mLength != mSuggestions.size()) {
31259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                        Log.e(TAG, "Suggestion size is not the same as stored mLength");
31359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                    }
314af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                    for (int i = mLength - 1; i >= 0; --i) {
315af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                        Log.i(TAG, "" + mScores[i] + " " + mSuggestions.get(i));
316af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                    }
3176b166a193398554694cb680f704c2ffc23d03a0eJean Chalard                }
31859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                Collections.reverse(mSuggestions);
319cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaoka                StringUtils.removeDupes(mSuggestions);
320f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                if (CAPITALIZE_ALL == capitalizeType) {
321f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                    for (int i = 0; i < mSuggestions.size(); ++i) {
322f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                        // get(i) returns a CharSequence which is actually a String so .toString()
323f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                        // should return the same object.
324f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                        mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale));
325f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                    }
326f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                } else if (CAPITALIZE_FIRST == capitalizeType) {
327f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                    for (int i = 0; i < mSuggestions.size(); ++i) {
328f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                        // Likewise
32911d9ee742f8ff3fb31b0e3beb32ee4870c63d8e3Tadashi G. Takaoka                        mSuggestions.set(i, StringUtils.toTitleCase(
3303bf57a5624679a20db26df912077a53b9f90ad36Tadashi G. Takaoka                                mSuggestions.get(i).toString(), locale));
331f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                    }
332f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard                }
33359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // This returns a String[], while toArray() returns an Object[] which cannot be cast
33459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                // into a String[].
33559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                gatheredSuggestions = mSuggestions.toArray(EMPTY_STRING_ARRAY);
33659b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
337af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                final int bestScore = mScores[mLength - 1];
33859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard                final CharSequence bestSuggestion = mSuggestions.get(0);
3390028ed3627ff4f37a62a80f3b2c857e373cd5090satok                final float normalizedScore =
340be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                        BinaryDictionary.calcNormalizedScore(
341be0cf72253f15bff6abdeaa79f60a56f06ab7b86satok                                mOriginalText, bestSuggestion.toString(), bestScore);
342a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
343af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                if (DBG) {
344af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                    Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore);
3454609c02f9e61370557fee675c67263160fbf7feeJean Chalard                    Log.i(TAG, "Normalized score = " + normalizedScore
346a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                            + " (threshold " + mRecommendedThreshold
347a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                            + ") => hasRecommendedSuggestions = " + hasRecommendedSuggestions);
348af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                }
3493234123fba901243990972158d023a5d1c273316Jean Chalard            }
350a409f009fa410019ad10b1134ff57393443eba33Jean Chalard            return new Result(gatheredSuggestions, hasRecommendedSuggestions);
3513234123fba901243990972158d023a5d1c273316Jean Chalard        }
3523234123fba901243990972158d023a5d1c273316Jean Chalard    }
3533234123fba901243990972158d023a5d1c273316Jean Chalard
354c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard    @Override
355c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard    public boolean onUnbind(final Intent intent) {
3568403611960cd0b2a40b77275c536e8088c098830Jean Chalard        closeAllDictionaries();
3578403611960cd0b2a40b77275c536e8088c098830Jean Chalard        return false;
3588403611960cd0b2a40b77275c536e8088c098830Jean Chalard    }
3598403611960cd0b2a40b77275c536e8088c098830Jean Chalard
3608403611960cd0b2a40b77275c536e8088c098830Jean Chalard    private void closeAllDictionaries() {
361c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard        final Map<String, DictionaryPool> oldPools = mDictionaryPools;
362c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
363150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
364150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
365fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
366fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
367c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard        for (DictionaryPool pool : oldPools.values()) {
368c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard            pool.close();
369c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard        }
370150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        for (Dictionary dict : oldUserDictionaries.values()) {
371150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard            dict.close();
372150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        }
373fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        for (Dictionary dict : oldWhitelistDictionaries.values()) {
374fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            dict.close();
375fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        }
37618222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang        synchronized (mUseContactsLock) {
377db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (null != mContactsDictionary) {
378db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                // The synchronously loaded contacts dictionary should have been in one
379db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                // or several pools, but it is shielded against multiple closing and it's
380db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                // safe to call it several times.
38118222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                final Dictionary dictToClose = mContactsDictionary;
38218222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no
38318222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                // longer needed
384db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                mContactsDictionary = null;
385db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                dictToClose.close();
386db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
3872e3c6da8688a907024d4d8e0f2db3e0ed4fab8dbJean Chalard        }
388c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard    }
389c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard
390a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    private DictionaryPool getDictionaryPool(final String locale) {
391a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard        DictionaryPool pool = mDictionaryPools.get(locale);
392a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard        if (null == pool) {
393ef35cb631c45c8b106fe7ed9e0d1178c3e5fb963Jean Chalard            final Locale localeObject = LocaleUtils.constructLocaleFromString(locale);
394a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard            pool = new DictionaryPool(POOL_SIZE, this, localeObject);
395a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard            mDictionaryPools.put(locale, pool);
3963234123fba901243990972158d023a5d1c273316Jean Chalard        }
397a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard        return pool;
398a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    }
399a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard
400a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard    public DictAndProximity createDictAndProximity(final Locale locale) {
4011830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        final int script = getScriptFromLocale(locale);
4021830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard        final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo(
40388794b24c0928e3bbea59999fce47c78c028863dKen Wakasa                SpellCheckerProximityInfo.getProximityForScript(script),
40488794b24c0928e3bbea59999fce47c78c028863dKen Wakasa                SpellCheckerProximityInfo.ROW_SIZE,
40588794b24c0928e3bbea59999fce47c78c028863dKen Wakasa                SpellCheckerProximityInfo.PROXIMITY_GRID_WIDTH,
40688794b24c0928e3bbea59999fce47c78c028863dKen Wakasa                SpellCheckerProximityInfo.PROXIMITY_GRID_HEIGHT);
407150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        final DictionaryCollection dictionaryCollection =
408f0e12a969974987f1b97929886c6ebe6a685c538Jean Chalard                DictionaryFactory.createMainDictionaryFromManager(this, locale,
40924aee9100e92dc4c06cdb54487a4922420fa8660Jean Chalard                        true /* useFullEditDistance */);
410150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        final String localeStr = locale.toString();
411fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        Dictionary userDictionary = mUserDictionaries.get(localeStr);
412fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        if (null == userDictionary) {
413f6adff6227a15af105dbf39c57213a24bf16780bTom Ouyang            if (LatinIME.USE_BINARY_USER_DICTIONARY) {
414f6adff6227a15af105dbf39c57213a24bf16780bTom Ouyang                userDictionary = new SynchronouslyLoadedUserBinaryDictionary(this, localeStr, true);
415f6adff6227a15af105dbf39c57213a24bf16780bTom Ouyang            } else {
416f6adff6227a15af105dbf39c57213a24bf16780bTom Ouyang                userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
417f6adff6227a15af105dbf39c57213a24bf16780bTom Ouyang            }
418fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            mUserDictionaries.put(localeStr, userDictionary);
419fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        }
420fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        dictionaryCollection.addDictionary(userDictionary);
421fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr);
422fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        if (null == whitelistDictionary) {
423fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            whitelistDictionary = new WhitelistDictionary(this, locale);
424fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard            mWhitelistDictionaries.put(localeStr, whitelistDictionary);
425150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        }
426fee149abe0358ff0efcebff3d0b60d8be83af437Jean Chalard        dictionaryCollection.addDictionary(whitelistDictionary);
42718222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang        synchronized (mUseContactsLock) {
428db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            if (mUseContactsDictionary) {
429db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                if (null == mContactsDictionary) {
43018222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    // TODO: revert to the concrete type when USE_BINARY_CONTACTS_DICTIONARY is no
43118222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    // longer needed
43218222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    if (LatinIME.USE_BINARY_CONTACTS_DICTIONARY) {
43318222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                        mContactsDictionary = new SynchronouslyLoadedContactsBinaryDictionary(this);
43418222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    } else {
43518222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                        mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
43618222f8c863e509538857b1fafca9c696fae2f55Tom Ouyang                    }
437db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                }
438db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            }
439db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            dictionaryCollection.addDictionary(mContactsDictionary);
440db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard            mDictionaryCollectionsList.add(
441db5aedb5a5eea5224e5a732b689c97eead2e35f4Jean Chalard                    new WeakReference<DictionaryCollection>(dictionaryCollection));
4422e3c6da8688a907024d4d8e0f2db3e0ed4fab8dbJean Chalard        }
443150bad6fd4b401177c480acf5640b4db0f821886Jean Chalard        return new DictAndProximity(dictionaryCollection, proximityInfo);
4443234123fba901243990972158d023a5d1c273316Jean Chalard    }
4453234123fba901243990972158d023a5d1c273316Jean Chalard
446f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    // This method assumes the text is not empty or null.
447f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    private static int getCapitalizationType(String text) {
448f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // If the first char is not uppercase, then the word is either all lower case,
449f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // and in either case we return CAPITALIZE_NONE.
450f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE;
4519242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard        final int len = text.length();
452f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        int capsCount = 1;
4539242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard        for (int i = 1; i < len; i = text.offsetByCodePoints(i, 1)) {
454f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard            if (1 != capsCount && i != capsCount) break;
455f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard            if (Character.isUpperCase(text.codePointAt(i))) ++capsCount;
456f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        }
457f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // We know the first char is upper case. So we want to test if either everything
458f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // else is lower case, or if everything else is upper case. If the string is
459f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // exactly one char long, then we will arrive here with capsCount 1, and this is
460f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        // correct, too.
461f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        if (1 == capsCount) return CAPITALIZE_FIRST;
462f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard        return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
463f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard    }
464f5ef30dfc6f4e436d35c38b6f7e32fbd24d54aabJean Chalard
46559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard    private static class AndroidSpellCheckerSession extends Session {
466a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard        // Immutable, but need the locale which is not available in the constructor yet
46759b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private DictionaryPool mDictionaryPool;
4685d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard        // Likewise
46959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private Locale mLocale;
470bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard        // Cache this for performance
471bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard        private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
47259b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
47359b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        private final AndroidSpellCheckerService mService;
47459b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard
47574a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        private final SuggestionsCache mSuggestionsCache = new SuggestionsCache();
47674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
47774a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        private static class SuggestionsParams {
47874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public final String[] mSuggestions;
47974a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public final int mFlags;
48074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public SuggestionsParams(String[] suggestions, int flags) {
48174a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                mSuggestions = suggestions;
48274a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                mFlags = flags;
48374a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            }
48474a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        }
48574a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
48674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        private static class SuggestionsCache {
48774a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            private static final int MAX_CACHE_SIZE = 50;
48874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            // TODO: support bigram
48974a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            private final LruCache<String, SuggestionsParams> mUnigramSuggestionsInfoCache =
49074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    new LruCache<String, SuggestionsParams>(MAX_CACHE_SIZE);
49174a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
49274a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public SuggestionsParams getSuggestionsFromCache(String query) {
49374a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                return mUnigramSuggestionsInfoCache.get(query);
49474a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            }
49574a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
49674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            public void putSuggestionsToCache(String query, String[] suggestions, int flags) {
49774a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                if (suggestions == null || TextUtils.isEmpty(query)) {
49874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    return;
49974a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                }
50074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                mUnigramSuggestionsInfoCache.put(query, new SuggestionsParams(suggestions, flags));
50174a84febc76d1ec6c0b6d8afbf50349da9b38d74satok            }
5025434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok
5035434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            public void remove(String key) {
5045434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                mUnigramSuggestionsInfoCache.remove(key);
5055434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
50674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok        }
50774a84febc76d1ec6c0b6d8afbf50349da9b38d74satok
50859b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        AndroidSpellCheckerSession(final AndroidSpellCheckerService service) {
50959b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            mService = service;
51059b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard        }
511a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard
5125bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        @Override
5135bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        public void onCreate() {
5145d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard            final String localeString = getLocale();
51559b501a05078e5a9de7cdace19c51ca693076a17Jean Chalard            mDictionaryPool = mService.getDictionaryPool(localeString);
516ef35cb631c45c8b106fe7ed9e0d1178c3e5fb963Jean Chalard            mLocale = LocaleUtils.constructLocaleFromString(localeString);
5171830cd1dc8259aa57175f1cf2a3d8797a7a35935Jean Chalard            mScript = getScriptFromLocale(mLocale);
518a90992e56244a914195daba3a2dd8a0e66e63384satok        }
5193234123fba901243990972158d023a5d1c273316Jean Chalard
52072479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard        /*
52172479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard         * Returns whether the code point is a letter that makes sense for the specified
52272479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard         * locale for this spell checker.
52372479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard         * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
524bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * and is limited to EFIGS languages and Russian.
525bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
526bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
52772479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard         */
52872479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard        private static boolean isLetterCheckableByLanguage(final int codePoint,
529bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                final int script) {
530bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            switch (script) {
531bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            case SCRIPT_LATIN:
532bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // Our supported latin script dictionaries (EFIGS) at the moment only include
533bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
534bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
535bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
536bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // excluded from isLetter anyway.
537bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                return codePoint <= 0x2AF && Character.isLetter(codePoint);
538bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            case SCRIPT_CYRILLIC:
539bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // All Cyrillic characters are in the 400~52F block. There are some in the upper
540bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // Unicode range, but they are archaic characters that are not used in modern
541bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // russian and are not used by our dictionary.
542bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
543bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            default:
544bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                // Should never come here
545bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                throw new RuntimeException("Impossible value of script: " + script);
546bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            }
54772479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard        }
54872479ea3636a7f9379ff40ae673fc67255abab6dJean Chalard
54988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard        /**
55088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         * Finds out whether a particular string should be filtered out of spell checking.
55188fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         *
552bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
553bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * we know we will never recognize, this accepts a script identifier that should be one
554bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * of the SCRIPT_* constants defined above, to rule out quickly characters from very
555bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * different languages.
55688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         *
55788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         * @param text the string to evaluate.
558bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard         * @param script the identifier for the script this spell checker recognizes
55988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         * @return true if we should filter this text out, false otherwise
56088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard         */
561bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard        private static boolean shouldFilterOut(final String text, final int script) {
56288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
56388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard
56488fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // TODO: check if an equivalent processing can't be done more quickly with a
56588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // compiled regexp.
56688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // Filter by first letter
56788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            final int firstCodePoint = text.codePointAt(0);
56888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // Filter out words that don't start with a letter or an apostrophe
569bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard            if (!isLetterCheckableByLanguage(firstCodePoint, script)
57088fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                    && '\'' != firstCodePoint) return true;
57188fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard
57288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // Filter contents
57388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            final int length = text.length();
57488fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            int letterCount = 0;
5759242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard            for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
57688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                final int codePoint = text.codePointAt(i);
57788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                // Any word containing a '@' is probably an e-mail address
57888fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                // Any word containing a '/' is probably either an ad-hoc combination of two
57988fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard                // words or a URI - in either case we don't want to spell check that
5809242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard                if ('@' == codePoint || '/' == codePoint) return true;
581bb2b30fc7ff31182d314e4db9baf1913bf08522dJean Chalard                if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
58288fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            }
58388fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
58488fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            // in this word are letters
58588fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard            return (letterCount * 4 < length * 3);
58688fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard        }
58788fa53b840686bb428b932eed7dd38162ae902c2Jean Chalard
5885434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(
5895434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                TextInfo ti, SentenceSuggestionsInfo ssi) {
5905434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final String typedText = ti.getText();
5915434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            if (!typedText.contains(SINGLE_QUOTE)) {
5925434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                return null;
5935434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
5945434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int N = ssi.getSuggestionsCount();
5955434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final ArrayList<Integer> additionalOffsets = new ArrayList<Integer>();
5965434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final ArrayList<Integer> additionalLengths = new ArrayList<Integer>();
5975434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final ArrayList<SuggestionsInfo> additionalSuggestionsInfos =
5985434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    new ArrayList<SuggestionsInfo>();
5995434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            for (int i = 0; i < N; ++i) {
6005434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
6015434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final int flags = si.getSuggestionsAttributes();
6025434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
6035434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    continue;
6045434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6055434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final int offset = ssi.getOffsetAt(i);
6065434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final int length = ssi.getLengthAt(i);
6075434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final String subText = typedText.substring(offset, offset + length);
6085434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                if (!subText.contains(SINGLE_QUOTE)) {
6095434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    continue;
6105434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6115434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final String[] splitTexts = subText.split(SINGLE_QUOTE, -1);
6125434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                if (splitTexts == null || splitTexts.length <= 1) {
6135434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    continue;
6145434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6155434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final int splitNum = splitTexts.length;
6165434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                for (int j = 0; j < splitNum; ++j) {
6175434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    final String splitText = splitTexts[j];
6185434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    if (TextUtils.isEmpty(splitText)) {
6195434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                        continue;
6205434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    }
6215434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    if (mSuggestionsCache.getSuggestionsFromCache(splitText) == null) {
6225434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                        continue;
6235434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    }
6245434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    final int newLength = splitText.length();
6255434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
6265434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    final int newFlags = 0;
6275434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
6285434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
6295434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    if (DBG) {
6305434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                        Log.d(TAG, "Override and remove old span over: "
6315434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                                + splitText + ", " + offset + "," + newLength);
6325434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    }
6335434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    additionalOffsets.add(offset);
6345434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    additionalLengths.add(newLength);
6355434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    additionalSuggestionsInfos.add(newSi);
6365434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6375434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6385434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int additionalSize = additionalOffsets.size();
6395434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            if (additionalSize <= 0) {
6405434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                return null;
6415434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6425434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int suggestionsSize = N + additionalSize;
6435434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int[] newOffsets = new int[suggestionsSize];
6445434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final int[] newLengths = new int[suggestionsSize];
6455434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
6465434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            int i;
6475434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            for (i = 0; i < N; ++i) {
6485434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newOffsets[i] = ssi.getOffsetAt(i);
6495434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newLengths[i] = ssi.getLengthAt(i);
6505434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
6515434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6525434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            for (; i < suggestionsSize; ++i) {
6535434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newOffsets[i] = additionalOffsets.get(i - N);
6545434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newLengths[i] = additionalLengths.get(i - N);
6555434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
6565434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6575434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
6585434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        }
6595434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok
6605434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        @Override
6615434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(
6625434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                TextInfo[] textInfos, int suggestionsLimit) {
6635434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            final SentenceSuggestionsInfo[] retval = super.onGetSentenceSuggestionsMultiple(
6645434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    textInfos, suggestionsLimit);
6655434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            if (retval == null || retval.length != textInfos.length) {
6665434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                return retval;
6675434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6685434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            for (int i = 0; i < retval.length; ++i) {
6695434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                final SentenceSuggestionsInfo tempSsi =
6705434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                        fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
6715434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                if (tempSsi != null) {
6725434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                    retval[i] = tempSsi;
6735434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok                }
6745434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            }
6755434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok            return retval;
6765434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok        }
6775434f46481c6331c3f107e6940cb49ba9dd5ea4dsatok
678315d731d8f11929b6202020475a477024067c1f1satok        @Override
679315d731d8f11929b6202020475a477024067c1f1satok        public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
680315d731d8f11929b6202020475a477024067c1f1satok                int suggestionsLimit, boolean sequentialWords) {
681315d731d8f11929b6202020475a477024067c1f1satok            final int length = textInfos.length;
682315d731d8f11929b6202020475a477024067c1f1satok            final SuggestionsInfo[] retval = new SuggestionsInfo[length];
683315d731d8f11929b6202020475a477024067c1f1satok            for (int i = 0; i < length; ++i) {
684315d731d8f11929b6202020475a477024067c1f1satok                final String prevWord;
685315d731d8f11929b6202020475a477024067c1f1satok                if (sequentialWords && i > 0) {
686315d731d8f11929b6202020475a477024067c1f1satok                    final String prevWordCandidate = textInfos[i - 1].getText();
687315d731d8f11929b6202020475a477024067c1f1satok                    // Note that an empty string would be used to indicate the initial word
688315d731d8f11929b6202020475a477024067c1f1satok                    // in the future.
689315d731d8f11929b6202020475a477024067c1f1satok                    prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
690315d731d8f11929b6202020475a477024067c1f1satok                } else {
691315d731d8f11929b6202020475a477024067c1f1satok                    prevWord = null;
692315d731d8f11929b6202020475a477024067c1f1satok                }
693315d731d8f11929b6202020475a477024067c1f1satok                retval[i] = onGetSuggestions(textInfos[i], prevWord, suggestionsLimit);
694315d731d8f11929b6202020475a477024067c1f1satok                retval[i].setCookieAndSequence(
695315d731d8f11929b6202020475a477024067c1f1satok                        textInfos[i].getCookie(), textInfos[i].getSequence());
696315d731d8f11929b6202020475a477024067c1f1satok            }
697315d731d8f11929b6202020475a477024067c1f1satok            return retval;
698315d731d8f11929b6202020475a477024067c1f1satok        }
699315d731d8f11929b6202020475a477024067c1f1satok
7005bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        // Note : this must be reentrant
7015bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        /**
7025bcf8ee66ceb38675a6b70fefcb574978e0fae92satok         * Gets a list of suggestions for a specific string. This returns a list of possible
70370b9c5d9913b676f21fe29f795bdb25324509205Jean Chalard         * corrections for the text passed as an argument. It may split or group words, and
7045bcf8ee66ceb38675a6b70fefcb574978e0fae92satok         * even perform grammatical analysis.
7055bcf8ee66ceb38675a6b70fefcb574978e0fae92satok         */
7065bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        @Override
7075bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
7085bcf8ee66ceb38675a6b70fefcb574978e0fae92satok                final int suggestionsLimit) {
709315d731d8f11929b6202020475a477024067c1f1satok            return onGetSuggestions(textInfo, null, suggestionsLimit);
710315d731d8f11929b6202020475a477024067c1f1satok        }
711315d731d8f11929b6202020475a477024067c1f1satok
712315d731d8f11929b6202020475a477024067c1f1satok        private SuggestionsInfo onGetSuggestions(
713315d731d8f11929b6202020475a477024067c1f1satok                final TextInfo textInfo, final String prevWord, final int suggestionsLimit) {
714a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard            try {
715e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                final String inText = textInfo.getText();
71674a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                final SuggestionsParams cachedSuggestionsParams =
717e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                        mSuggestionsCache.getSuggestionsFromCache(inText);
71874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                if (cachedSuggestionsParams != null) {
71974a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    if (DBG) {
720e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                        Log.d(TAG, "Cache hit: " + inText + ", " + cachedSuggestionsParams.mFlags);
72174a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    }
72274a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                    return new SuggestionsInfo(
72374a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                            cachedSuggestionsParams.mFlags, cachedSuggestionsParams.mSuggestions);
72474a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                }
725199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard
726e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                if (shouldFilterOut(inText, mScript)) {
727a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    DictAndProximity dictInfo = null;
728a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    try {
729a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        dictInfo = mDictionaryPool.takeOrGetNull();
730cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard                        if (null == dictInfo) return getNotInDictEmptySuggestions();
731e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                        return dictInfo.mDictionary.isValidWord(inText) ?
732e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                                getInDictEmptySuggestions() : getNotInDictEmptySuggestions();
733a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    } finally {
734a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        if (null != dictInfo) {
735a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                            if (!mDictionaryPool.offer(dictInfo)) {
736a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                                Log.e(TAG, "Can't re-insert a dictionary into its pool");
737a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                            }
738a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        }
739a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    }
740e897e4d3422c8d9d8b6f051376cc2ba16e4d5945Jean Chalard                }
741e58f3af8a7bf852c3b100de1bd85d95d13e0e15esatok                final String text = inText.replaceAll(APOSTROPHE, SINGLE_QUOTE);
742199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard
743647db70fec321d9847f6568cc7bd2b3bd6671322Jean Chalard                // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
7444609c02f9e61370557fee675c67263160fbf7feeJean Chalard                final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
745a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                        mService.mSuggestionThreshold, mService.mRecommendedThreshold,
746a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                        suggestionsLimit);
747199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                final WordComposer composer = new WordComposer();
748199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                final int length = text.length();
7499242a2bcf8a6b07bb045a8356711bed1493c251eJean Chalard                for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
750081616cd2f472295449268cecb570771b969cba3Jean Chalard                    final int codePoint = text.codePointAt(i);
751081616cd2f472295449268cecb570771b969cba3Jean Chalard                    // The getXYForCodePointAndScript method returns (Y << 16) + X
752081616cd2f472295449268cecb570771b969cba3Jean Chalard                    final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
753081616cd2f472295449268cecb570771b969cba3Jean Chalard                            codePoint, mScript);
754b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                    if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
755b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                        composer.add(codePoint, WordComposer.NOT_A_COORDINATE,
756b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                                WordComposer.NOT_A_COORDINATE, null);
757b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                    } else {
758b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                        composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
759b0b89c87f60a8b6515d830ff5b36866fc64b7a26Jean Chalard                    }
7605d4c5692f11958064ba7c0de5715f30c96175400Jean Chalard                }
761199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard
762199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                final int capitalizeType = getCapitalizationType(text);
763199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                boolean isInDict = true;
764a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                DictAndProximity dictInfo = null;
765a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                try {
766a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    dictInfo = mDictionaryPool.takeOrGetNull();
767cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard                    if (null == dictInfo) return getNotInDictEmptySuggestions();
768315d731d8f11929b6202020475a477024067c1f1satok                    dictInfo.mDictionary.getWords(composer, prevWord, suggestionsGatherer,
769a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                            dictInfo.mProximityInfo);
770a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    isInDict = dictInfo.mDictionary.isValidWord(text);
771a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    if (!isInDict && CAPITALIZE_NONE != capitalizeType) {
772a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        // We want to test the word again if it's all caps or first caps only.
773a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        // If it's fully down, we already tested it, if it's mixed case, we don't
774a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        // want to test a lowercase version of it.
775a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
776a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    }
777a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                } finally {
778a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    if (null != dictInfo) {
779a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        if (!mDictionaryPool.offer(dictInfo)) {
780a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                            Log.e(TAG, "Can't re-insert a dictionary into its pool");
781a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                        }
782a9876980c87748750d3edb19d72ff65bce75f024Jean Chalard                    }
783c160373b6a8e8a536ad8aa2798a33a41d3050f3bJean Chalard                }
7845bcf8ee66ceb38675a6b70fefcb574978e0fae92satok
78585782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard                final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
78685782abaf178f6aafa1f8999123ff540f04c17bcJean Chalard                        capitalizeType, mLocale);
787199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard
788199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                if (DBG) {
789199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                    Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
790199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                            + suggestionsLimit);
791647db70fec321d9847f6568cc7bd2b3bd6671322Jean Chalard                    Log.i(TAG, "IsInDict = " + isInDict);
792647db70fec321d9847f6568cc7bd2b3bd6671322Jean Chalard                    Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
793a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                    Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
79451075d145a85d1acaff08c02f4d6b10b175eaa36Jean Chalard                    if (null != result.mSuggestions) {
79551075d145a85d1acaff08c02f4d6b10b175eaa36Jean Chalard                        for (String suggestion : result.mSuggestions) {
79651075d145a85d1acaff08c02f4d6b10b175eaa36Jean Chalard                            Log.i(TAG, suggestion);
79751075d145a85d1acaff08c02f4d6b10b175eaa36Jean Chalard                        }
798199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                    }
799199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                }
800a562767a14c7bbac95b25e69e360fc28d6ce9e33Jean Chalard
801199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                final int flags =
802647db70fec321d9847f6568cc7bd2b3bd6671322Jean Chalard                        (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
803a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                                : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
804a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                        | (result.mHasRecommendedSuggestions
8059260422423819ed6942f11c03960d5764e97c262Ken Wakasa                                ? SuggestionsInfoCompatUtils
8069260422423819ed6942f11c03960d5764e97c262Ken Wakasa                                        .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
807a409f009fa410019ad10b1134ff57393443eba33Jean Chalard                                : 0);
80874a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                final SuggestionsInfo retval = new SuggestionsInfo(flags, result.mSuggestions);
80974a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                mSuggestionsCache.putSuggestionsToCache(text, result.mSuggestions, flags);
81074a84febc76d1ec6c0b6d8afbf50349da9b38d74satok                return retval;
811199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard            } catch (RuntimeException e) {
812199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                // Don't kill the keyboard if there is a bug in the spell checker
813199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                if (DBG) {
814199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                    throw e;
815199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                } else {
816199dc5e0e4236eed408650dbb0dc07d7f16bbe03Jean Chalard                    Log.e(TAG, "Exception while spellcheking: " + e);
817cba1af9c5626a2cb1e611735deb72db72d02c4c1Jean Chalard                    return getNotInDictEmptySuggestions();
818af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard                }
819af3b56c887b6c0a1bcbb21c50489f2d7ae65f654Jean Chalard            }
8205bcf8ee66ceb38675a6b70fefcb574978e0fae92satok        }
821022c1cc20379767966f4915e2dea65fc0b67c0d8satok    }
822022c1cc20379767966f4915e2dea65fc0b67c0d8satok}
823