AndroidSpellCheckerService.java revision 24aee9100e92dc4c06cdb54487a4922420fa8660
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 * use this file except in compliance with the License. You may obtain a copy of
6 * the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations under
14 * the License.
15 */
16
17package com.android.inputmethod.latin.spellcheck;
18
19import android.content.Intent;
20import android.content.SharedPreferences;
21import android.content.res.Resources;
22import android.preference.PreferenceManager;
23import android.service.textservice.SpellCheckerService;
24import android.text.TextUtils;
25import android.util.Log;
26import android.view.textservice.SuggestionsInfo;
27import android.view.textservice.TextInfo;
28
29import com.android.inputmethod.compat.SuggestionsInfoCompatUtils;
30import com.android.inputmethod.keyboard.ProximityInfo;
31import com.android.inputmethod.latin.BinaryDictionary;
32import com.android.inputmethod.latin.Dictionary;
33import com.android.inputmethod.latin.Dictionary.WordCallback;
34import com.android.inputmethod.latin.DictionaryCollection;
35import com.android.inputmethod.latin.DictionaryFactory;
36import com.android.inputmethod.latin.Flag;
37import com.android.inputmethod.latin.LocaleUtils;
38import com.android.inputmethod.latin.R;
39import com.android.inputmethod.latin.StringUtils;
40import com.android.inputmethod.latin.SynchronouslyLoadedContactsDictionary;
41import com.android.inputmethod.latin.SynchronouslyLoadedUserDictionary;
42import com.android.inputmethod.latin.WhitelistDictionary;
43import com.android.inputmethod.latin.WordComposer;
44
45import java.lang.ref.WeakReference;
46import java.util.ArrayList;
47import java.util.Arrays;
48import java.util.Collections;
49import java.util.HashSet;
50import java.util.Iterator;
51import java.util.Locale;
52import java.util.Map;
53import java.util.TreeMap;
54
55/**
56 * Service for spell checking, using LatinIME's dictionaries and mechanisms.
57 */
58public class AndroidSpellCheckerService extends SpellCheckerService
59        implements SharedPreferences.OnSharedPreferenceChangeListener {
60    private static final String TAG = AndroidSpellCheckerService.class.getSimpleName();
61    private static final boolean DBG = false;
62    private static final int POOL_SIZE = 2;
63
64    public static final String PREF_USE_CONTACTS_KEY = "pref_spellcheck_use_contacts";
65
66    private static final int CAPITALIZE_NONE = 0; // No caps, or mixed case
67    private static final int CAPITALIZE_FIRST = 1; // First only
68    private static final int CAPITALIZE_ALL = 2; // All caps
69
70    private final static String[] EMPTY_STRING_ARRAY = new String[0];
71    private Map<String, DictionaryPool> mDictionaryPools =
72            Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
73    private Map<String, Dictionary> mUserDictionaries =
74            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
75    private Map<String, Dictionary> mWhitelistDictionaries =
76            Collections.synchronizedMap(new TreeMap<String, Dictionary>());
77    private SynchronouslyLoadedContactsDictionary mContactsDictionary;
78
79    // The threshold for a candidate to be offered as a suggestion.
80    private double mSuggestionThreshold;
81    // The threshold for a suggestion to be considered "recommended".
82    private double mRecommendedThreshold;
83    // Whether to use the contacts dictionary
84    private boolean mUseContactsDictionary;
85    private final Object mUseContactsLock = new Object();
86
87    private final HashSet<WeakReference<DictionaryCollection>> mDictionaryCollectionsList =
88            new HashSet<WeakReference<DictionaryCollection>>();
89
90    public static final int SCRIPT_LATIN = 0;
91    public static final int SCRIPT_CYRILLIC = 1;
92    private static final TreeMap<String, Integer> mLanguageToScript;
93    static {
94        // List of the supported languages and their associated script. We won't check
95        // words written in another script than the selected script, because we know we
96        // don't have those in our dictionary so we will underline everything and we
97        // will never have any suggestions, so it makes no sense checking them.
98        mLanguageToScript = new TreeMap<String, Integer>();
99        mLanguageToScript.put("en", SCRIPT_LATIN);
100        mLanguageToScript.put("fr", SCRIPT_LATIN);
101        mLanguageToScript.put("de", SCRIPT_LATIN);
102        mLanguageToScript.put("nl", SCRIPT_LATIN);
103        mLanguageToScript.put("cs", SCRIPT_LATIN);
104        mLanguageToScript.put("es", SCRIPT_LATIN);
105        mLanguageToScript.put("it", SCRIPT_LATIN);
106        mLanguageToScript.put("ru", SCRIPT_CYRILLIC);
107    }
108
109    @Override public void onCreate() {
110        super.onCreate();
111        mSuggestionThreshold =
112                Double.parseDouble(getString(R.string.spellchecker_suggestion_threshold_value));
113        mRecommendedThreshold =
114                Double.parseDouble(getString(R.string.spellchecker_recommended_threshold_value));
115        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
116        prefs.registerOnSharedPreferenceChangeListener(this);
117        onSharedPreferenceChanged(prefs, PREF_USE_CONTACTS_KEY);
118    }
119
120    private static int getScriptFromLocale(final Locale locale) {
121        final Integer script = mLanguageToScript.get(locale.getLanguage());
122        if (null == script) {
123            throw new RuntimeException("We have been called with an unsupported language: \""
124                    + locale.getLanguage() + "\". Framework bug?");
125        }
126        return script;
127    }
128
129    @Override
130    public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
131        if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
132        synchronized(mUseContactsLock) {
133            mUseContactsDictionary = prefs.getBoolean(PREF_USE_CONTACTS_KEY, true);
134            if (mUseContactsDictionary) {
135                startUsingContactsDictionaryLocked();
136            } else {
137                stopUsingContactsDictionaryLocked();
138            }
139        }
140    }
141
142    private void startUsingContactsDictionaryLocked() {
143        if (null == mContactsDictionary) {
144            mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
145        }
146        final Iterator<WeakReference<DictionaryCollection>> iterator =
147                mDictionaryCollectionsList.iterator();
148        while (iterator.hasNext()) {
149            final WeakReference<DictionaryCollection> dictRef = iterator.next();
150            final DictionaryCollection dict = dictRef.get();
151            if (null == dict) {
152                iterator.remove();
153            } else {
154                dict.addDictionary(mContactsDictionary);
155            }
156        }
157    }
158
159    private void stopUsingContactsDictionaryLocked() {
160        if (null == mContactsDictionary) return;
161        final SynchronouslyLoadedContactsDictionary contactsDict = mContactsDictionary;
162        mContactsDictionary = null;
163        final Iterator<WeakReference<DictionaryCollection>> iterator =
164                mDictionaryCollectionsList.iterator();
165        while (iterator.hasNext()) {
166            final WeakReference<DictionaryCollection> dictRef = iterator.next();
167            final DictionaryCollection dict = dictRef.get();
168            if (null == dict) {
169                iterator.remove();
170            } else {
171                dict.removeDictionary(contactsDict);
172            }
173        }
174        contactsDict.close();
175    }
176
177    @Override
178    public Session createSession() {
179        return new AndroidSpellCheckerSession(this);
180    }
181
182    private static SuggestionsInfo getNotInDictEmptySuggestions() {
183        return new SuggestionsInfo(0, EMPTY_STRING_ARRAY);
184    }
185
186    private static SuggestionsInfo getInDictEmptySuggestions() {
187        return new SuggestionsInfo(SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY,
188                EMPTY_STRING_ARRAY);
189    }
190
191    private static class SuggestionsGatherer implements WordCallback {
192        public static class Result {
193            public final String[] mSuggestions;
194            public final boolean mHasRecommendedSuggestions;
195            public Result(final String[] gatheredSuggestions,
196                    final boolean hasRecommendedSuggestions) {
197                mSuggestions = gatheredSuggestions;
198                mHasRecommendedSuggestions = hasRecommendedSuggestions;
199            }
200        }
201
202        private final ArrayList<CharSequence> mSuggestions;
203        private final int[] mScores;
204        private final String mOriginalText;
205        private final double mSuggestionThreshold;
206        private final double mRecommendedThreshold;
207        private final int mMaxLength;
208        private int mLength = 0;
209
210        // The two following attributes are only ever filled if the requested max length
211        // is 0 (or less, which is treated the same).
212        private String mBestSuggestion = null;
213        private int mBestScore = Integer.MIN_VALUE; // As small as possible
214
215        SuggestionsGatherer(final String originalText, final double suggestionThreshold,
216                final double recommendedThreshold, final int maxLength) {
217            mOriginalText = originalText;
218            mSuggestionThreshold = suggestionThreshold;
219            mRecommendedThreshold = recommendedThreshold;
220            mMaxLength = maxLength;
221            mSuggestions = new ArrayList<CharSequence>(maxLength + 1);
222            mScores = new int[mMaxLength];
223        }
224
225        @Override
226        synchronized public boolean addWord(char[] word, int wordOffset, int wordLength, int score,
227                int dicTypeId, int dataType) {
228            final int positionIndex = Arrays.binarySearch(mScores, 0, mLength, score);
229            // binarySearch returns the index if the element exists, and -<insertion index> - 1
230            // if it doesn't. See documentation for binarySearch.
231            final int insertIndex = positionIndex >= 0 ? positionIndex : -positionIndex - 1;
232
233            if (insertIndex == 0 && mLength >= mMaxLength) {
234                // In the future, we may want to keep track of the best suggestion score even if
235                // we are asked for 0 suggestions. In this case, we can use the following
236                // (tested) code to keep it:
237                // If the maxLength is 0 (should never be less, but if it is, it's treated as 0)
238                // then we need to keep track of the best suggestion in mBestScore and
239                // mBestSuggestion. This is so that we know whether the best suggestion makes
240                // the score cutoff, since we need to know that to return a meaningful
241                // looksLikeTypo.
242                // if (0 >= mMaxLength) {
243                //     if (score > mBestScore) {
244                //         mBestScore = score;
245                //         mBestSuggestion = new String(word, wordOffset, wordLength);
246                //     }
247                // }
248                return true;
249            }
250            if (insertIndex >= mMaxLength) {
251                // We found a suggestion, but its score is too weak to be kept considering
252                // the suggestion limit.
253                return true;
254            }
255
256            // Compute the normalized score and skip this word if it's normalized score does not
257            // make the threshold.
258            final String wordString = new String(word, wordOffset, wordLength);
259            final double normalizedScore =
260                    BinaryDictionary.calcNormalizedScore(mOriginalText, wordString, score);
261            if (normalizedScore < mSuggestionThreshold) {
262                if (DBG) Log.i(TAG, wordString + " does not make the score threshold");
263                return true;
264            }
265
266            if (mLength < mMaxLength) {
267                final int copyLen = mLength - insertIndex;
268                ++mLength;
269                System.arraycopy(mScores, insertIndex, mScores, insertIndex + 1, copyLen);
270                mSuggestions.add(insertIndex, wordString);
271            } else {
272                System.arraycopy(mScores, 1, mScores, 0, insertIndex);
273                mSuggestions.add(insertIndex, wordString);
274                mSuggestions.remove(0);
275            }
276            mScores[insertIndex] = score;
277
278            return true;
279        }
280
281        public Result getResults(final int capitalizeType, final Locale locale) {
282            final String[] gatheredSuggestions;
283            final boolean hasRecommendedSuggestions;
284            if (0 == mLength) {
285                // Either we found no suggestions, or we found some BUT the max length was 0.
286                // If we found some mBestSuggestion will not be null. If it is null, then
287                // we found none, regardless of the max length.
288                if (null == mBestSuggestion) {
289                    gatheredSuggestions = null;
290                    hasRecommendedSuggestions = false;
291                } else {
292                    gatheredSuggestions = EMPTY_STRING_ARRAY;
293                    final double normalizedScore = BinaryDictionary.calcNormalizedScore(
294                            mOriginalText, mBestSuggestion, mBestScore);
295                    hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
296                }
297            } else {
298                if (DBG) {
299                    if (mLength != mSuggestions.size()) {
300                        Log.e(TAG, "Suggestion size is not the same as stored mLength");
301                    }
302                    for (int i = mLength - 1; i >= 0; --i) {
303                        Log.i(TAG, "" + mScores[i] + " " + mSuggestions.get(i));
304                    }
305                }
306                Collections.reverse(mSuggestions);
307                StringUtils.removeDupes(mSuggestions);
308                if (CAPITALIZE_ALL == capitalizeType) {
309                    for (int i = 0; i < mSuggestions.size(); ++i) {
310                        // get(i) returns a CharSequence which is actually a String so .toString()
311                        // should return the same object.
312                        mSuggestions.set(i, mSuggestions.get(i).toString().toUpperCase(locale));
313                    }
314                } else if (CAPITALIZE_FIRST == capitalizeType) {
315                    for (int i = 0; i < mSuggestions.size(); ++i) {
316                        // Likewise
317                        mSuggestions.set(i, StringUtils.toTitleCase(
318                                mSuggestions.get(i).toString(), locale));
319                    }
320                }
321                // This returns a String[], while toArray() returns an Object[] which cannot be cast
322                // into a String[].
323                gatheredSuggestions = mSuggestions.toArray(EMPTY_STRING_ARRAY);
324
325                final int bestScore = mScores[mLength - 1];
326                final CharSequence bestSuggestion = mSuggestions.get(0);
327                final double normalizedScore =
328                        BinaryDictionary.calcNormalizedScore(
329                                mOriginalText, bestSuggestion.toString(), bestScore);
330                hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
331                if (DBG) {
332                    Log.i(TAG, "Best suggestion : " + bestSuggestion + ", score " + bestScore);
333                    Log.i(TAG, "Normalized score = " + normalizedScore
334                            + " (threshold " + mRecommendedThreshold
335                            + ") => hasRecommendedSuggestions = " + hasRecommendedSuggestions);
336                }
337            }
338            return new Result(gatheredSuggestions, hasRecommendedSuggestions);
339        }
340    }
341
342    @Override
343    public boolean onUnbind(final Intent intent) {
344        closeAllDictionaries();
345        return false;
346    }
347
348    private void closeAllDictionaries() {
349        final Map<String, DictionaryPool> oldPools = mDictionaryPools;
350        mDictionaryPools = Collections.synchronizedMap(new TreeMap<String, DictionaryPool>());
351        final Map<String, Dictionary> oldUserDictionaries = mUserDictionaries;
352        mUserDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
353        final Map<String, Dictionary> oldWhitelistDictionaries = mWhitelistDictionaries;
354        mWhitelistDictionaries = Collections.synchronizedMap(new TreeMap<String, Dictionary>());
355        for (DictionaryPool pool : oldPools.values()) {
356            pool.close();
357        }
358        for (Dictionary dict : oldUserDictionaries.values()) {
359            dict.close();
360        }
361        for (Dictionary dict : oldWhitelistDictionaries.values()) {
362            dict.close();
363        }
364        synchronized(mUseContactsLock) {
365            if (null != mContactsDictionary) {
366                // The synchronously loaded contacts dictionary should have been in one
367                // or several pools, but it is shielded against multiple closing and it's
368                // safe to call it several times.
369                final SynchronouslyLoadedContactsDictionary dictToClose = mContactsDictionary;
370                mContactsDictionary = null;
371                dictToClose.close();
372            }
373        }
374    }
375
376    private DictionaryPool getDictionaryPool(final String locale) {
377        DictionaryPool pool = mDictionaryPools.get(locale);
378        if (null == pool) {
379            final Locale localeObject = LocaleUtils.constructLocaleFromString(locale);
380            pool = new DictionaryPool(POOL_SIZE, this, localeObject);
381            mDictionaryPools.put(locale, pool);
382        }
383        return pool;
384    }
385
386    public DictAndProximity createDictAndProximity(final Locale locale) {
387        final int script = getScriptFromLocale(locale);
388        final ProximityInfo proximityInfo = ProximityInfo.createSpellCheckerProximityInfo(
389                SpellCheckerProximityInfo.getProximityForScript(script));
390        final Resources resources = getResources();
391        final int fallbackResourceId = DictionaryFactory.getMainDictionaryResourceId(resources);
392        final DictionaryCollection dictionaryCollection =
393                DictionaryFactory.createDictionaryFromManager(this, locale, fallbackResourceId,
394                        true /* useFullEditDistance */);
395        final String localeStr = locale.toString();
396        Dictionary userDictionary = mUserDictionaries.get(localeStr);
397        if (null == userDictionary) {
398            userDictionary = new SynchronouslyLoadedUserDictionary(this, localeStr, true);
399            mUserDictionaries.put(localeStr, userDictionary);
400        }
401        dictionaryCollection.addDictionary(userDictionary);
402        Dictionary whitelistDictionary = mWhitelistDictionaries.get(localeStr);
403        if (null == whitelistDictionary) {
404            whitelistDictionary = new WhitelistDictionary(this, locale);
405            mWhitelistDictionaries.put(localeStr, whitelistDictionary);
406        }
407        dictionaryCollection.addDictionary(whitelistDictionary);
408        synchronized(mUseContactsLock) {
409            if (mUseContactsDictionary) {
410                if (null == mContactsDictionary) {
411                    mContactsDictionary = new SynchronouslyLoadedContactsDictionary(this);
412                }
413            }
414            dictionaryCollection.addDictionary(mContactsDictionary);
415            mDictionaryCollectionsList.add(
416                    new WeakReference<DictionaryCollection>(dictionaryCollection));
417        }
418        return new DictAndProximity(dictionaryCollection, proximityInfo);
419    }
420
421    // This method assumes the text is not empty or null.
422    private static int getCapitalizationType(String text) {
423        // If the first char is not uppercase, then the word is either all lower case,
424        // and in either case we return CAPITALIZE_NONE.
425        if (!Character.isUpperCase(text.codePointAt(0))) return CAPITALIZE_NONE;
426        final int len = text.length();
427        int capsCount = 1;
428        for (int i = 1; i < len; i = text.offsetByCodePoints(i, 1)) {
429            if (1 != capsCount && i != capsCount) break;
430            if (Character.isUpperCase(text.codePointAt(i))) ++capsCount;
431        }
432        // We know the first char is upper case. So we want to test if either everything
433        // else is lower case, or if everything else is upper case. If the string is
434        // exactly one char long, then we will arrive here with capsCount 1, and this is
435        // correct, too.
436        if (1 == capsCount) return CAPITALIZE_FIRST;
437        return (len == capsCount ? CAPITALIZE_ALL : CAPITALIZE_NONE);
438    }
439
440    private static class AndroidSpellCheckerSession extends Session {
441        // Immutable, but need the locale which is not available in the constructor yet
442        private DictionaryPool mDictionaryPool;
443        // Likewise
444        private Locale mLocale;
445        // Cache this for performance
446        private int mScript; // One of SCRIPT_LATIN or SCRIPT_CYRILLIC for now.
447
448        private final AndroidSpellCheckerService mService;
449
450        AndroidSpellCheckerSession(final AndroidSpellCheckerService service) {
451            mService = service;
452        }
453
454        @Override
455        public void onCreate() {
456            final String localeString = getLocale();
457            mDictionaryPool = mService.getDictionaryPool(localeString);
458            mLocale = LocaleUtils.constructLocaleFromString(localeString);
459            mScript = getScriptFromLocale(mLocale);
460        }
461
462        /*
463         * Returns whether the code point is a letter that makes sense for the specified
464         * locale for this spell checker.
465         * The dictionaries supported by Latin IME are described in res/xml/spellchecker.xml
466         * and is limited to EFIGS languages and Russian.
467         * Hence at the moment this explicitly tests for Cyrillic characters or Latin characters
468         * as appropriate, and explicitly excludes CJK, Arabic and Hebrew characters.
469         */
470        private static boolean isLetterCheckableByLanguage(final int codePoint,
471                final int script) {
472            switch (script) {
473            case SCRIPT_LATIN:
474                // Our supported latin script dictionaries (EFIGS) at the moment only include
475                // characters in the C0, C1, Latin Extended A and B, IPA extensions unicode
476                // blocks. As it happens, those are back-to-back in the code range 0x40 to 0x2AF,
477                // so the below is a very efficient way to test for it. As for the 0-0x3F, it's
478                // excluded from isLetter anyway.
479                return codePoint <= 0x2AF && Character.isLetter(codePoint);
480            case SCRIPT_CYRILLIC:
481                // All Cyrillic characters are in the 400~52F block. There are some in the upper
482                // Unicode range, but they are archaic characters that are not used in modern
483                // russian and are not used by our dictionary.
484                return codePoint >= 0x400 && codePoint <= 0x52F && Character.isLetter(codePoint);
485            default:
486                // Should never come here
487                throw new RuntimeException("Impossible value of script: " + script);
488            }
489        }
490
491        /**
492         * Finds out whether a particular string should be filtered out of spell checking.
493         *
494         * This will loosely match URLs, numbers, symbols. To avoid always underlining words that
495         * we know we will never recognize, this accepts a script identifier that should be one
496         * of the SCRIPT_* constants defined above, to rule out quickly characters from very
497         * different languages.
498         *
499         * @param text the string to evaluate.
500         * @param script the identifier for the script this spell checker recognizes
501         * @return true if we should filter this text out, false otherwise
502         */
503        private static boolean shouldFilterOut(final String text, final int script) {
504            if (TextUtils.isEmpty(text) || text.length() <= 1) return true;
505
506            // TODO: check if an equivalent processing can't be done more quickly with a
507            // compiled regexp.
508            // Filter by first letter
509            final int firstCodePoint = text.codePointAt(0);
510            // Filter out words that don't start with a letter or an apostrophe
511            if (!isLetterCheckableByLanguage(firstCodePoint, script)
512                    && '\'' != firstCodePoint) return true;
513
514            // Filter contents
515            final int length = text.length();
516            int letterCount = 0;
517            for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
518                final int codePoint = text.codePointAt(i);
519                // Any word containing a '@' is probably an e-mail address
520                // Any word containing a '/' is probably either an ad-hoc combination of two
521                // words or a URI - in either case we don't want to spell check that
522                if ('@' == codePoint || '/' == codePoint) return true;
523                if (isLetterCheckableByLanguage(codePoint, script)) ++letterCount;
524            }
525            // Guestimate heuristic: perform spell checking if at least 3/4 of the characters
526            // in this word are letters
527            return (letterCount * 4 < length * 3);
528        }
529
530        // Note : this must be reentrant
531        /**
532         * Gets a list of suggestions for a specific string. This returns a list of possible
533         * corrections for the text passed as an argument. It may split or group words, and
534         * even perform grammatical analysis.
535         */
536        @Override
537        public SuggestionsInfo onGetSuggestions(final TextInfo textInfo,
538                final int suggestionsLimit) {
539            try {
540                final String text = textInfo.getText();
541
542                if (shouldFilterOut(text, mScript)) {
543                    DictAndProximity dictInfo = null;
544                    try {
545                        dictInfo = mDictionaryPool.takeOrGetNull();
546                        if (null == dictInfo) return getNotInDictEmptySuggestions();
547                        return dictInfo.mDictionary.isValidWord(text) ? getInDictEmptySuggestions()
548                                : getNotInDictEmptySuggestions();
549                    } finally {
550                        if (null != dictInfo) {
551                            if (!mDictionaryPool.offer(dictInfo)) {
552                                Log.e(TAG, "Can't re-insert a dictionary into its pool");
553                            }
554                        }
555                    }
556                }
557
558                // TODO: Don't gather suggestions if the limit is <= 0 unless necessary
559                final SuggestionsGatherer suggestionsGatherer = new SuggestionsGatherer(text,
560                        mService.mSuggestionThreshold, mService.mRecommendedThreshold,
561                        suggestionsLimit);
562                final WordComposer composer = new WordComposer();
563                final int length = text.length();
564                for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
565                    final int codePoint = text.codePointAt(i);
566                    // The getXYForCodePointAndScript method returns (Y << 16) + X
567                    final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
568                            codePoint, mScript);
569                    if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
570                        composer.add(codePoint, WordComposer.NOT_A_COORDINATE,
571                                WordComposer.NOT_A_COORDINATE, null);
572                    } else {
573                        composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
574                    }
575                }
576
577                final int capitalizeType = getCapitalizationType(text);
578                boolean isInDict = true;
579                DictAndProximity dictInfo = null;
580                try {
581                    dictInfo = mDictionaryPool.takeOrGetNull();
582                    if (null == dictInfo) return getNotInDictEmptySuggestions();
583                    dictInfo.mDictionary.getWords(composer, suggestionsGatherer,
584                            dictInfo.mProximityInfo);
585                    isInDict = dictInfo.mDictionary.isValidWord(text);
586                    if (!isInDict && CAPITALIZE_NONE != capitalizeType) {
587                        // We want to test the word again if it's all caps or first caps only.
588                        // If it's fully down, we already tested it, if it's mixed case, we don't
589                        // want to test a lowercase version of it.
590                        isInDict = dictInfo.mDictionary.isValidWord(text.toLowerCase(mLocale));
591                    }
592                } finally {
593                    if (null != dictInfo) {
594                        if (!mDictionaryPool.offer(dictInfo)) {
595                            Log.e(TAG, "Can't re-insert a dictionary into its pool");
596                        }
597                    }
598                }
599
600                final SuggestionsGatherer.Result result = suggestionsGatherer.getResults(
601                        capitalizeType, mLocale);
602
603                if (DBG) {
604                    Log.i(TAG, "Spell checking results for " + text + " with suggestion limit "
605                            + suggestionsLimit);
606                    Log.i(TAG, "IsInDict = " + isInDict);
607                    Log.i(TAG, "LooksLikeTypo = " + (!isInDict));
608                    Log.i(TAG, "HasRecommendedSuggestions = " + result.mHasRecommendedSuggestions);
609                    if (null != result.mSuggestions) {
610                        for (String suggestion : result.mSuggestions) {
611                            Log.i(TAG, suggestion);
612                        }
613                    }
614                }
615
616                final int flags =
617                        (isInDict ? SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY
618                                : SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO)
619                        | (result.mHasRecommendedSuggestions
620                                ? SuggestionsInfoCompatUtils
621                                        .getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS()
622                                : 0);
623                return new SuggestionsInfo(flags, result.mSuggestions);
624            } catch (RuntimeException e) {
625                // Don't kill the keyboard if there is a bug in the spell checker
626                if (DBG) {
627                    throw e;
628                } else {
629                    Log.e(TAG, "Exception while spellcheking: " + e);
630                    return getNotInDictEmptySuggestions();
631                }
632            }
633        }
634    }
635}
636