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