Suggest.java revision 0028ed3627ff4f37a62a80f3b2c857e373cd5090
1/*
2 * Copyright (C) 2008 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;
18
19import android.content.Context;
20import android.text.TextUtils;
21import android.util.Log;
22
23import com.android.inputmethod.keyboard.Keyboard;
24import com.android.inputmethod.keyboard.ProximityInfo;
25import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
26
27import java.io.File;
28import java.util.ArrayList;
29import java.util.HashMap;
30import java.util.HashSet;
31import java.util.Locale;
32
33/**
34 * This class loads a dictionary and provides a list of suggestions for a given sequence of
35 * characters. This includes corrections and completions.
36 */
37public class Suggest implements Dictionary.WordCallback {
38    public static final String TAG = Suggest.class.getSimpleName();
39
40    public static final int APPROX_MAX_WORD_LENGTH = 32;
41
42    public static final int CORRECTION_NONE = 0;
43    public static final int CORRECTION_FULL = 1;
44    public static final int CORRECTION_FULL_BIGRAM = 2;
45
46    // It seems the following values are only used for logging.
47    public static final int DIC_USER_TYPED = 0;
48    public static final int DIC_MAIN = 1;
49    public static final int DIC_USER = 2;
50    public static final int DIC_USER_HISTORY = 3;
51    public static final int DIC_CONTACTS = 4;
52    public static final int DIC_WHITELIST = 6;
53    // If you add a type of dictionary, increment DIC_TYPE_LAST_ID
54    // TODO: this value seems unused. Remove it?
55    public static final int DIC_TYPE_LAST_ID = 6;
56    public static final String DICT_KEY_MAIN = "main";
57    public static final String DICT_KEY_CONTACTS = "contacts";
58    // User dictionary, the system-managed one.
59    public static final String DICT_KEY_USER = "user";
60    // User history dictionary for the unigram map, internal to LatinIME
61    public static final String DICT_KEY_USER_HISTORY_UNIGRAM = "history_unigram";
62    // User history dictionary for the bigram map, internal to LatinIME
63    public static final String DICT_KEY_USER_HISTORY_BIGRAM = "history_bigram";
64    public static final String DICT_KEY_WHITELIST ="whitelist";
65
66    private static final boolean DBG = LatinImeLogger.sDBG;
67
68    private boolean mHasMainDictionary;
69    private Dictionary mContactsDict;
70    private WhitelistDictionary mWhiteListDictionary;
71    private final HashMap<String, Dictionary> mUnigramDictionaries =
72            new HashMap<String, Dictionary>();
73    private final HashMap<String, Dictionary> mBigramDictionaries =
74            new HashMap<String, Dictionary>();
75
76    private int mPrefMaxSuggestions = 18;
77
78    private static final int PREF_MAX_BIGRAMS = 60;
79
80    private float mAutoCorrectionThreshold;
81
82    private ArrayList<SuggestedWordInfo> mSuggestions = new ArrayList<SuggestedWordInfo>();
83    private ArrayList<SuggestedWordInfo> mBigramSuggestions = new ArrayList<SuggestedWordInfo>();
84    private CharSequence mConsideredWord;
85
86    // TODO: Remove these member variables by passing more context to addWord() callback method
87    private boolean mIsFirstCharCapitalized;
88    private boolean mIsAllUpperCase;
89    private int mTrailingSingleQuotesCount;
90
91    private static final int MINIMUM_SAFETY_NET_CHAR_LENGTH = 4;
92
93    public Suggest(final Context context, final Locale locale) {
94        initAsynchronously(context, locale);
95    }
96
97    /* package for test */ Suggest(final Context context, final File dictionary,
98            final long startOffset, final long length, final Locale locale) {
99        final Dictionary mainDict = DictionaryFactory.createDictionaryForTest(context, dictionary,
100                startOffset, length /* useFullEditDistance */, false, locale);
101        mHasMainDictionary = null != mainDict;
102        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, mainDict);
103        addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, mainDict);
104        initWhitelistAndAutocorrectAndPool(context, locale);
105    }
106
107    private void initWhitelistAndAutocorrectAndPool(final Context context, final Locale locale) {
108        mWhiteListDictionary = new WhitelistDictionary(context, locale);
109        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_WHITELIST, mWhiteListDictionary);
110    }
111
112    private void initAsynchronously(final Context context, final Locale locale) {
113        resetMainDict(context, locale);
114
115        // TODO: read the whitelist and init the pool asynchronously too.
116        // initPool should be done asynchronously now that the pool is thread-safe.
117        initWhitelistAndAutocorrectAndPool(context, locale);
118    }
119
120    private static void addOrReplaceDictionary(HashMap<String, Dictionary> dictionaries, String key,
121            Dictionary dict) {
122        final Dictionary oldDict = (dict == null)
123                ? dictionaries.remove(key)
124                : dictionaries.put(key, dict);
125        if (oldDict != null && dict != oldDict) {
126            oldDict.close();
127        }
128    }
129
130    public void resetMainDict(final Context context, final Locale locale) {
131        mHasMainDictionary = false;
132        new Thread("InitializeBinaryDictionary") {
133            @Override
134            public void run() {
135                final DictionaryCollection newMainDict =
136                        DictionaryFactory.createMainDictionaryFromManager(context, locale);
137                mHasMainDictionary = null != newMainDict && !newMainDict.isEmpty();
138                addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_MAIN, newMainDict);
139                addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_MAIN, newMainDict);
140            }
141        }.start();
142    }
143
144    // The main dictionary could have been loaded asynchronously.  Don't cache the return value
145    // of this method.
146    public boolean hasMainDictionary() {
147        return mHasMainDictionary;
148    }
149
150    public Dictionary getContactsDictionary() {
151        return mContactsDict;
152    }
153
154    public HashMap<String, Dictionary> getUnigramDictionaries() {
155        return mUnigramDictionaries;
156    }
157
158    public static int getApproxMaxWordLength() {
159        return APPROX_MAX_WORD_LENGTH;
160    }
161
162    /**
163     * Sets an optional user dictionary resource to be loaded. The user dictionary is consulted
164     * before the main dictionary, if set. This refers to the system-managed user dictionary.
165     */
166    public void setUserDictionary(Dictionary userDictionary) {
167        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER, userDictionary);
168    }
169
170    /**
171     * Sets an optional contacts dictionary resource to be loaded. It is also possible to remove
172     * the contacts dictionary by passing null to this method. In this case no contacts dictionary
173     * won't be used.
174     */
175    public void setContactsDictionary(Dictionary contactsDictionary) {
176        mContactsDict = contactsDictionary;
177        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
178        addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_CONTACTS, contactsDictionary);
179    }
180
181    public void setUserHistoryDictionary(Dictionary userHistoryDictionary) {
182        addOrReplaceDictionary(mUnigramDictionaries, DICT_KEY_USER_HISTORY_UNIGRAM,
183                userHistoryDictionary);
184        addOrReplaceDictionary(mBigramDictionaries, DICT_KEY_USER_HISTORY_BIGRAM,
185                userHistoryDictionary);
186    }
187
188    public void setAutoCorrectionThreshold(float threshold) {
189        mAutoCorrectionThreshold = threshold;
190    }
191
192    private static CharSequence capitalizeWord(final boolean all, final boolean first,
193            final CharSequence word) {
194        if (TextUtils.isEmpty(word) || !(all || first)) return word;
195        final int wordLength = word.length();
196        final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
197        // TODO: Must pay attention to locale when changing case.
198        if (all) {
199            sb.append(word.toString().toUpperCase());
200        } else if (first) {
201            sb.append(Character.toUpperCase(word.charAt(0)));
202            if (wordLength > 1) {
203                sb.append(word.subSequence(1, wordLength));
204            }
205        }
206        return sb;
207    }
208
209    protected void addBigramToSuggestions(SuggestedWordInfo bigram) {
210        mSuggestions.add(bigram);
211    }
212
213    private static final WordComposer sEmptyWordComposer = new WordComposer();
214    public SuggestedWords getBigramPredictions(CharSequence prevWordForBigram) {
215        LatinImeLogger.onStartSuggestion(prevWordForBigram);
216        mIsFirstCharCapitalized = false;
217        mIsAllUpperCase = false;
218        mTrailingSingleQuotesCount = 0;
219        mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
220
221        // Treating USER_TYPED as UNIGRAM suggestion for logging now.
222        LatinImeLogger.onAddSuggestedWord("", Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
223        mConsideredWord = "";
224
225        mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
226
227        getAllBigrams(prevWordForBigram, sEmptyWordComposer);
228
229        // Nothing entered: return all bigrams for the previous word
230        int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
231        for (int i = 0; i < insertCount; ++i) {
232            addBigramToSuggestions(mBigramSuggestions.get(i));
233        }
234
235        SuggestedWordInfo.removeDups(mSuggestions);
236
237        return new SuggestedWords(mSuggestions,
238                false /* typedWordValid */,
239                false /* hasAutoCorrectionCandidate */,
240                false /* allowsToBeAutoCorrected */,
241                false /* isPunctuationSuggestions */,
242                false /* isObsoleteSuggestions */,
243                true /* isPrediction */);
244    }
245
246    // TODO: cleanup dictionaries looking up and suggestions building with SuggestedWords.Builder
247    public SuggestedWords getSuggestedWords(
248            final WordComposer wordComposer, CharSequence prevWordForBigram,
249            final ProximityInfo proximityInfo, final int correctionMode) {
250        LatinImeLogger.onStartSuggestion(prevWordForBigram);
251        mIsFirstCharCapitalized = wordComposer.isFirstCharCapitalized();
252        mIsAllUpperCase = wordComposer.isAllUpperCase();
253        mTrailingSingleQuotesCount = wordComposer.trailingSingleQuotesCount();
254        mSuggestions = new ArrayList<SuggestedWordInfo>(mPrefMaxSuggestions);
255
256        final String typedWord = wordComposer.getTypedWord();
257        final String consideredWord = mTrailingSingleQuotesCount > 0
258                ? typedWord.substring(0, typedWord.length() - mTrailingSingleQuotesCount)
259                : typedWord;
260        // Treating USER_TYPED as UNIGRAM suggestion for logging now.
261        LatinImeLogger.onAddSuggestedWord(typedWord, Suggest.DIC_USER_TYPED, Dictionary.UNIGRAM);
262        mConsideredWord = consideredWord;
263
264        if (wordComposer.size() <= 1 && (correctionMode == CORRECTION_FULL_BIGRAM)) {
265            // At first character typed, search only the bigrams
266            mBigramSuggestions = new ArrayList<SuggestedWordInfo>(PREF_MAX_BIGRAMS);
267
268            if (!TextUtils.isEmpty(prevWordForBigram)) {
269                getAllBigrams(prevWordForBigram, wordComposer);
270                if (TextUtils.isEmpty(consideredWord)) {
271                    // Nothing entered: return all bigrams for the previous word
272                    int insertCount = Math.min(mBigramSuggestions.size(), mPrefMaxSuggestions);
273                    for (int i = 0; i < insertCount; ++i) {
274                        addBigramToSuggestions(mBigramSuggestions.get(i));
275                    }
276                } else {
277                    // Word entered: return only bigrams that match the first char of the typed word
278                    final char currentChar = consideredWord.charAt(0);
279                    // TODO: Must pay attention to locale when changing case.
280                    // TODO: Use codepoint instead of char
281                    final char currentCharUpper = Character.toUpperCase(currentChar);
282                    int count = 0;
283                    final int bigramSuggestionSize = mBigramSuggestions.size();
284                    for (int i = 0; i < bigramSuggestionSize; i++) {
285                        final SuggestedWordInfo bigramSuggestion = mBigramSuggestions.get(i);
286                        final char bigramSuggestionFirstChar =
287                                (char)bigramSuggestion.codePointAt(0);
288                        if (bigramSuggestionFirstChar == currentChar
289                                || bigramSuggestionFirstChar == currentCharUpper) {
290                            addBigramToSuggestions(bigramSuggestion);
291                            if (++count > mPrefMaxSuggestions) break;
292                        }
293                    }
294                }
295            }
296
297        } else if (wordComposer.size() > 1) {
298            final WordComposer wordComposerForLookup;
299            if (mTrailingSingleQuotesCount > 0) {
300                wordComposerForLookup = new WordComposer(wordComposer);
301                for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
302                    wordComposerForLookup.deleteLast();
303                }
304            } else {
305                wordComposerForLookup = wordComposer;
306            }
307            // At second character typed, search the unigrams (scores being affected by bigrams)
308            for (final String key : mUnigramDictionaries.keySet()) {
309                // Skip UserUnigramDictionary and WhitelistDictionary to lookup
310                if (key.equals(DICT_KEY_USER_HISTORY_UNIGRAM) || key.equals(DICT_KEY_WHITELIST))
311                    continue;
312                final Dictionary dictionary = mUnigramDictionaries.get(key);
313                dictionary.getWords(wordComposerForLookup, prevWordForBigram, this, proximityInfo);
314            }
315        }
316
317        final CharSequence whitelistedWord = capitalizeWord(mIsAllUpperCase,
318                mIsFirstCharCapitalized, mWhiteListDictionary.getWhitelistedWord(consideredWord));
319
320        final boolean hasAutoCorrection;
321        if (CORRECTION_FULL == correctionMode || CORRECTION_FULL_BIGRAM == correctionMode) {
322            final CharSequence autoCorrection =
323                    AutoCorrection.computeAutoCorrectionWord(mUnigramDictionaries, wordComposer,
324                            mSuggestions, consideredWord, mAutoCorrectionThreshold,
325                            whitelistedWord);
326            hasAutoCorrection = (null != autoCorrection);
327        } else {
328            hasAutoCorrection = false;
329        }
330
331        if (whitelistedWord != null) {
332            if (mTrailingSingleQuotesCount > 0) {
333                final StringBuilder sb = new StringBuilder(whitelistedWord);
334                for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
335                    sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
336                }
337                mSuggestions.add(0, new SuggestedWordInfo(
338                        sb.toString(), SuggestedWordInfo.MAX_SCORE));
339            } else {
340                mSuggestions.add(0, new SuggestedWordInfo(
341                        whitelistedWord, SuggestedWordInfo.MAX_SCORE));
342            }
343        }
344
345        mSuggestions.add(0, new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE));
346        SuggestedWordInfo.removeDups(mSuggestions);
347
348        final ArrayList<SuggestedWordInfo> suggestionsList;
349        if (DBG) {
350            suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, mSuggestions);
351        } else {
352            suggestionsList = mSuggestions;
353        }
354
355        // TODO: Change this scheme - a boolean is not enough. A whitelisted word may be "valid"
356        // but still autocorrected from - in the case the whitelist only capitalizes the word.
357        // The whitelist should be case-insensitive, so it's not possible to be consistent with
358        // a boolean flag. Right now this is handled with a slight hack in
359        // WhitelistDictionary#shouldForciblyAutoCorrectFrom.
360        final boolean allowsToBeAutoCorrected = AutoCorrection.allowsToBeAutoCorrected(
361                getUnigramDictionaries(), consideredWord, wordComposer.isFirstCharCapitalized())
362        // If we don't have a main dictionary, we never want to auto-correct. The reason for this
363        // is, the user may have a contact whose name happens to match a valid word in their
364        // language, and it will unexpectedly auto-correct. For example, if the user types in
365        // English with no dictionary and has a "Will" in their contact list, "will" would
366        // always auto-correct to "Will" which is unwanted. Hence, no main dict => no auto-correct.
367                && mHasMainDictionary;
368
369        boolean autoCorrectionAvailable = hasAutoCorrection;
370        if (correctionMode == CORRECTION_FULL || correctionMode == CORRECTION_FULL_BIGRAM) {
371            autoCorrectionAvailable |= !allowsToBeAutoCorrected;
372        }
373        // Don't auto-correct words with multiple capital letter
374        autoCorrectionAvailable &= !wordComposer.isMostlyCaps();
375        autoCorrectionAvailable &= !wordComposer.isResumed();
376        if (allowsToBeAutoCorrected && suggestionsList.size() > 1 && mAutoCorrectionThreshold > 0
377                && Suggest.shouldBlockAutoCorrectionBySafetyNet(typedWord,
378                        suggestionsList.get(1).mWord)) {
379            autoCorrectionAvailable = false;
380        }
381        return new SuggestedWords(suggestionsList,
382                !allowsToBeAutoCorrected /* typedWordValid */,
383                autoCorrectionAvailable /* hasAutoCorrectionCandidate */,
384                allowsToBeAutoCorrected /* allowsToBeAutoCorrected */,
385                false /* isPunctuationSuggestions */,
386                false /* isObsoleteSuggestions */,
387                false /* isPrediction */);
388    }
389
390    /**
391     * Adds all bigram predictions for prevWord. Also checks the lower case version of prevWord if
392     * it contains any upper case characters.
393     */
394    private void getAllBigrams(final CharSequence prevWord, final WordComposer wordComposer) {
395        if (StringUtils.hasUpperCase(prevWord)) {
396            // TODO: Must pay attention to locale when changing case.
397            final CharSequence lowerPrevWord = prevWord.toString().toLowerCase();
398            for (final Dictionary dictionary : mBigramDictionaries.values()) {
399                dictionary.getBigrams(wordComposer, lowerPrevWord, this);
400            }
401        }
402        for (final Dictionary dictionary : mBigramDictionaries.values()) {
403            dictionary.getBigrams(wordComposer, prevWord, this);
404        }
405    }
406
407    private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
408            final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) {
409        final SuggestedWordInfo typedWordInfo = suggestions.get(0);
410        typedWordInfo.setDebugString("+");
411        final int suggestionsSize = suggestions.size();
412        final ArrayList<SuggestedWordInfo> suggestionsList =
413                new ArrayList<SuggestedWordInfo>(suggestionsSize);
414        suggestionsList.add(typedWordInfo);
415        // Note: i here is the index in mScores[], but the index in mSuggestions is one more
416        // than i because we added the typed word to mSuggestions without touching mScores.
417        for (int i = 0; i < suggestionsSize - 1; ++i) {
418            final SuggestedWordInfo cur = suggestions.get(i + 1);
419            final float normalizedScore = BinaryDictionary.calcNormalizedScore(
420                    typedWord, cur.toString(), cur.mScore);
421            final String scoreInfoString;
422            if (normalizedScore > 0) {
423                scoreInfoString = String.format("%d (%4.2f)", cur.mScore, normalizedScore);
424            } else {
425                scoreInfoString = Integer.toString(cur.mScore);
426            }
427            cur.setDebugString(scoreInfoString);
428            suggestionsList.add(cur);
429        }
430        return suggestionsList;
431    }
432
433    // TODO: Use codepoint instead of char
434    @Override
435    public boolean addWord(final char[] word, final int offset, final int length, int score,
436            final int dicTypeId, final int dataType) {
437        int dataTypeForLog = dataType;
438        final ArrayList<SuggestedWordInfo> suggestions;
439        final int prefMaxSuggestions;
440        if (dataType == Dictionary.BIGRAM) {
441            suggestions = mBigramSuggestions;
442            prefMaxSuggestions = PREF_MAX_BIGRAMS;
443        } else {
444            suggestions = mSuggestions;
445            prefMaxSuggestions = mPrefMaxSuggestions;
446        }
447
448        int pos = 0;
449
450        // Check if it's the same word, only caps are different
451        if (StringUtils.equalsIgnoreCase(mConsideredWord, word, offset, length)) {
452            // TODO: remove this surrounding if clause and move this logic to
453            // getSuggestedWordBuilder.
454            if (suggestions.size() > 0) {
455                final SuggestedWordInfo currentHighestWord = suggestions.get(0);
456                // If the current highest word is also equal to typed word, we need to compare
457                // frequency to determine the insertion position. This does not ensure strictly
458                // correct ordering, but ensures the top score is on top which is enough for
459                // removing duplicates correctly.
460                if (StringUtils.equalsIgnoreCase(currentHighestWord.mWord, word, offset, length)
461                        && score <= currentHighestWord.mScore) {
462                    pos = 1;
463                }
464            }
465        } else {
466            // Check the last one's score and bail
467            if (suggestions.size() >= prefMaxSuggestions
468                    && suggestions.get(prefMaxSuggestions - 1).mScore >= score) return true;
469            while (pos < suggestions.size()) {
470                final int curScore = suggestions.get(pos).mScore;
471                if (curScore < score
472                        || (curScore == score && length < suggestions.get(pos).codePointCount())) {
473                    break;
474                }
475                pos++;
476            }
477        }
478        if (pos >= prefMaxSuggestions) {
479            return true;
480        }
481
482        final StringBuilder sb = new StringBuilder(getApproxMaxWordLength());
483        // TODO: Must pay attention to locale when changing case.
484        if (mIsAllUpperCase) {
485            sb.append(new String(word, offset, length).toUpperCase());
486        } else if (mIsFirstCharCapitalized) {
487            sb.append(Character.toUpperCase(word[offset]));
488            if (length > 1) {
489                sb.append(word, offset + 1, length - 1);
490            }
491        } else {
492            sb.append(word, offset, length);
493        }
494        for (int i = mTrailingSingleQuotesCount - 1; i >= 0; --i) {
495            sb.appendCodePoint(Keyboard.CODE_SINGLE_QUOTE);
496        }
497        suggestions.add(pos, new SuggestedWordInfo(sb, score));
498        if (suggestions.size() > prefMaxSuggestions) {
499            suggestions.remove(prefMaxSuggestions);
500        } else {
501            LatinImeLogger.onAddSuggestedWord(sb.toString(), dicTypeId, dataTypeForLog);
502        }
503        return true;
504    }
505
506    // TODO: Use codepoint instead of char
507    private int searchBigramSuggestion(final char[] word, final int offset, final int length) {
508        // TODO This is almost O(n^2). Might need fix.
509        // search whether the word appeared in bigram data
510        int bigramSuggestSize = mBigramSuggestions.size();
511        for (int i = 0; i < bigramSuggestSize; i++) {
512            if (mBigramSuggestions.get(i).codePointCount() == length) {
513                boolean chk = true;
514                for (int j = 0; j < length; j++) {
515                    if (mBigramSuggestions.get(i).codePointAt(j) != word[offset+j]) {
516                        chk = false;
517                        break;
518                    }
519                }
520                if (chk) return i;
521            }
522        }
523
524        return -1;
525    }
526
527    public void close() {
528        final HashSet<Dictionary> dictionaries = new HashSet<Dictionary>();
529        dictionaries.addAll(mUnigramDictionaries.values());
530        dictionaries.addAll(mBigramDictionaries.values());
531        for (final Dictionary dictionary : dictionaries) {
532            dictionary.close();
533        }
534        mHasMainDictionary = false;
535    }
536
537    // TODO: Resolve the inconsistencies between the native auto correction algorithms and
538    // this safety net
539    public static boolean shouldBlockAutoCorrectionBySafetyNet(final String typedWord,
540            final CharSequence suggestion) {
541        // Safety net for auto correction.
542        // Actually if we hit this safety net, it's a bug.
543        // If user selected aggressive auto correction mode, there is no need to use the safety
544        // net.
545        // If the length of typed word is less than MINIMUM_SAFETY_NET_CHAR_LENGTH,
546        // we should not use net because relatively edit distance can be big.
547        final int typedWordLength = typedWord.length();
548        if (typedWordLength < Suggest.MINIMUM_SAFETY_NET_CHAR_LENGTH) {
549            return false;
550        }
551        final int maxEditDistanceOfNativeDictionary =
552                (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
553        final int distance = BinaryDictionary.editDistance(typedWord, suggestion.toString());
554        if (DBG) {
555            Log.d(TAG, "Autocorrected edit distance = " + distance
556                    + ", " + maxEditDistanceOfNativeDictionary);
557        }
558        if (distance > maxEditDistanceOfNativeDictionary) {
559            if (DBG) {
560                Log.e(TAG, "Safety net: before = " + typedWord + ", after = " + suggestion);
561                Log.e(TAG, "(Error) The edit distance of this correction exceeds limit. "
562                        + "Turning off auto-correction.");
563            }
564            return true;
565        } else {
566            return false;
567        }
568    }
569}
570