• 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");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.inputmethod.latin;
18 
19 import android.text.TextUtils;
20 
21 import com.android.inputmethod.keyboard.ProximityInfo;
22 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
23 import com.android.inputmethod.latin.define.DebugFlags;
24 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
25 import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
26 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
27 import com.android.inputmethod.latin.utils.StringUtils;
28 import com.android.inputmethod.latin.utils.SuggestionResults;
29 
30 import java.util.ArrayList;
31 import 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  */
37 public final class Suggest {
38     public static final String TAG = Suggest.class.getSimpleName();
39 
40     // Session id for
41     // {@link #getSuggestedWords(WordComposer,String,ProximityInfo,boolean,int)}.
42     // We are sharing the same ID between typing and gesture to save RAM footprint.
43     public static final int SESSION_ID_TYPING = 0;
44     public static final int SESSION_ID_GESTURE = 0;
45 
46     // Close to -2**31
47     private static final int SUPPRESS_SUGGEST_THRESHOLD = -2000000000;
48 
49     private static final boolean DBG = DebugFlags.DEBUG_ENABLED;
50     private final DictionaryFacilitator mDictionaryFacilitator;
51 
52     private float mAutoCorrectionThreshold;
53 
Suggest(final DictionaryFacilitator dictionaryFacilitator)54     public Suggest(final DictionaryFacilitator dictionaryFacilitator) {
55         mDictionaryFacilitator = dictionaryFacilitator;
56     }
57 
getLocale()58     public Locale getLocale() {
59         return mDictionaryFacilitator.getLocale();
60     }
61 
setAutoCorrectionThreshold(final float threshold)62     public void setAutoCorrectionThreshold(final float threshold) {
63         mAutoCorrectionThreshold = threshold;
64     }
65 
66     public interface OnGetSuggestedWordsCallback {
onGetSuggestedWords(final SuggestedWords suggestedWords)67         public void onGetSuggestedWords(final SuggestedWords suggestedWords);
68     }
69 
getSuggestedWords(final WordComposer wordComposer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final boolean isCorrectionEnabled, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback)70     public void getSuggestedWords(final WordComposer wordComposer,
71             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
72             final SettingsValuesForSuggestion settingsValuesForSuggestion,
73             final boolean isCorrectionEnabled, final int inputStyle, final int sequenceNumber,
74             final OnGetSuggestedWordsCallback callback) {
75         if (wordComposer.isBatchMode()) {
76             getSuggestedWordsForBatchInput(wordComposer, prevWordsInfo, proximityInfo,
77                     settingsValuesForSuggestion, inputStyle, sequenceNumber, callback);
78         } else {
79             getSuggestedWordsForNonBatchInput(wordComposer, prevWordsInfo, proximityInfo,
80                     settingsValuesForSuggestion, inputStyle, isCorrectionEnabled,
81                     sequenceNumber, callback);
82         }
83     }
84 
getTransformedSuggestedWordInfoList( final WordComposer wordComposer, final SuggestionResults results, final int trailingSingleQuotesCount)85     private static ArrayList<SuggestedWordInfo> getTransformedSuggestedWordInfoList(
86             final WordComposer wordComposer, final SuggestionResults results,
87             final int trailingSingleQuotesCount) {
88         final boolean shouldMakeSuggestionsAllUpperCase = wordComposer.isAllUpperCase()
89                 && !wordComposer.isResumed();
90         final boolean isOnlyFirstCharCapitalized =
91                 wordComposer.isOrWillBeOnlyFirstCharCapitalized();
92 
93         final ArrayList<SuggestedWordInfo> suggestionsContainer = new ArrayList<>(results);
94         final int suggestionsCount = suggestionsContainer.size();
95         if (isOnlyFirstCharCapitalized || shouldMakeSuggestionsAllUpperCase
96                 || 0 != trailingSingleQuotesCount) {
97             for (int i = 0; i < suggestionsCount; ++i) {
98                 final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
99                 final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
100                         wordInfo, results.mLocale, shouldMakeSuggestionsAllUpperCase,
101                         isOnlyFirstCharCapitalized, trailingSingleQuotesCount);
102                 suggestionsContainer.set(i, transformedWordInfo);
103             }
104         }
105         return suggestionsContainer;
106     }
107 
getWhitelistedWordOrNull(final ArrayList<SuggestedWordInfo> suggestions)108     private static String getWhitelistedWordOrNull(final ArrayList<SuggestedWordInfo> suggestions) {
109         if (suggestions.isEmpty()) {
110             return null;
111         }
112         final SuggestedWordInfo firstSuggestedWordInfo = suggestions.get(0);
113         if (!firstSuggestedWordInfo.isKindOf(SuggestedWordInfo.KIND_WHITELIST)) {
114             return null;
115         }
116         return firstSuggestedWordInfo.mWord;
117     }
118 
119     // Retrieves suggestions for non-batch input (typing, recorrection, predictions...)
120     // and calls the callback function with the suggestions.
getSuggestedWordsForNonBatchInput(final WordComposer wordComposer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled, final int sequenceNumber, final OnGetSuggestedWordsCallback callback)121     private void getSuggestedWordsForNonBatchInput(final WordComposer wordComposer,
122             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
123             final SettingsValuesForSuggestion settingsValuesForSuggestion,
124             final int inputStyleIfNotPrediction, final boolean isCorrectionEnabled,
125             final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
126         final String typedWord = wordComposer.getTypedWord();
127         final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(typedWord);
128         final String consideredWord = trailingSingleQuotesCount > 0
129                 ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount)
130                 : typedWord;
131 
132         final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
133                 wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion,
134                 SESSION_ID_TYPING);
135         final ArrayList<SuggestedWordInfo> suggestionsContainer =
136                 getTransformedSuggestedWordInfoList(wordComposer, suggestionResults,
137                         trailingSingleQuotesCount);
138         final boolean didRemoveTypedWord =
139                 SuggestedWordInfo.removeDups(wordComposer.getTypedWord(), suggestionsContainer);
140 
141         final String whitelistedWord = getWhitelistedWordOrNull(suggestionsContainer);
142         final boolean resultsArePredictions = !wordComposer.isComposingWord();
143 
144         // We allow auto-correction if we have a whitelisted word, or if the word had more than
145         // one char and was not suggested.
146         final boolean allowsToBeAutoCorrected = (null != whitelistedWord)
147                 || (consideredWord.length() > 1 && !didRemoveTypedWord);
148 
149         final boolean hasAutoCorrection;
150         // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because
151         // any attempt to do auto-correction is already shielded with a test for this flag; at the
152         // same time, it feels wrong that the SuggestedWord object includes information about
153         // the current settings. It may also be useful to know, when the setting is off, whether
154         // the word *would* have been auto-corrected.
155         if (!isCorrectionEnabled || !allowsToBeAutoCorrected || resultsArePredictions
156                 || suggestionResults.isEmpty() || wordComposer.hasDigits()
157                 || wordComposer.isMostlyCaps() || wordComposer.isResumed()
158                 || !mDictionaryFacilitator.hasInitializedMainDictionary()
159                 || suggestionResults.first().isKindOf(SuggestedWordInfo.KIND_SHORTCUT)) {
160             // If we don't have a main dictionary, we never want to auto-correct. The reason for
161             // this is, the user may have a contact whose name happens to match a valid word in
162             // their language, and it will unexpectedly auto-correct. For example, if the user
163             // types in English with no dictionary and has a "Will" in their contact list, "will"
164             // would always auto-correct to "Will" which is unwanted. Hence, no main dict => no
165             // auto-correct.
166             // Also, shortcuts should never auto-correct unless they are whitelist entries.
167             // TODO: we may want to have shortcut-only entries auto-correct in the future.
168             hasAutoCorrection = false;
169         } else {
170             hasAutoCorrection = AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold(
171                     suggestionResults.first(), consideredWord, mAutoCorrectionThreshold);
172         }
173 
174         if (!TextUtils.isEmpty(typedWord)) {
175             suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
176                     SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
177                     Dictionary.DICTIONARY_USER_TYPED,
178                     SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
179                     SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
180         }
181 
182         final ArrayList<SuggestedWordInfo> suggestionsList;
183         if (DBG && !suggestionsContainer.isEmpty()) {
184             suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, suggestionsContainer);
185         } else {
186             suggestionsList = suggestionsContainer;
187         }
188 
189         final int inputStyle;
190         if (resultsArePredictions) {
191             inputStyle = suggestionResults.mIsBeginningOfSentence
192                     ? SuggestedWords.INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION
193                     : SuggestedWords.INPUT_STYLE_PREDICTION;
194         } else {
195             inputStyle = inputStyleIfNotPrediction;
196         }
197         callback.onGetSuggestedWords(new SuggestedWords(suggestionsList,
198                 suggestionResults.mRawSuggestions,
199                 // TODO: this first argument is lying. If this is a whitelisted word which is an
200                 // actual word, it says typedWordValid = false, which looks wrong. We should either
201                 // rename the attribute or change the value.
202                 !resultsArePredictions && !allowsToBeAutoCorrected /* typedWordValid */,
203                 hasAutoCorrection /* willAutoCorrect */,
204                 false /* isObsoleteSuggestions */, inputStyle, sequenceNumber));
205     }
206 
207     // Retrieves suggestions for the batch input
208     // and calls the callback function with the suggestions.
getSuggestedWordsForBatchInput(final WordComposer wordComposer, final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, final SettingsValuesForSuggestion settingsValuesForSuggestion, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback)209     private void getSuggestedWordsForBatchInput(final WordComposer wordComposer,
210             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
211             final SettingsValuesForSuggestion settingsValuesForSuggestion,
212             final int inputStyle, final int sequenceNumber,
213             final OnGetSuggestedWordsCallback callback) {
214         final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
215                 wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion,
216                 SESSION_ID_GESTURE);
217         final ArrayList<SuggestedWordInfo> suggestionsContainer =
218                 new ArrayList<>(suggestionResults);
219         final int suggestionsCount = suggestionsContainer.size();
220         final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock();
221         final boolean isAllUpperCase = wordComposer.isAllUpperCase();
222         if (isFirstCharCapitalized || isAllUpperCase) {
223             for (int i = 0; i < suggestionsCount; ++i) {
224                 final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
225                 final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
226                         wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized,
227                         0 /* trailingSingleQuotesCount */);
228                 suggestionsContainer.set(i, transformedWordInfo);
229             }
230         }
231 
232         if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord,
233                 wordComposer.getRejectedBatchModeSuggestion())) {
234             final SuggestedWordInfo rejected = suggestionsContainer.remove(0);
235             suggestionsContainer.add(1, rejected);
236         }
237         SuggestedWordInfo.removeDups(null /* typedWord */, suggestionsContainer);
238 
239         // For some reason some suggestions with MIN_VALUE are making their way here.
240         // TODO: Find a more robust way to detect distractors.
241         for (int i = suggestionsContainer.size() - 1; i >= 0; --i) {
242             if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) {
243                 suggestionsContainer.remove(i);
244             }
245         }
246 
247         // In the batch input mode, the most relevant suggested word should act as a "typed word"
248         // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
249         // Note that because this method is never used to get predictions, there is no need to
250         // modify inputType such in getSuggestedWordsForNonBatchInput.
251         callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer,
252                 suggestionResults.mRawSuggestions,
253                 true /* typedWordValid */,
254                 false /* willAutoCorrect */,
255                 false /* isObsoleteSuggestions */,
256                 inputStyle, sequenceNumber));
257     }
258 
getSuggestionsInfoListWithDebugInfo( final String typedWord, final ArrayList<SuggestedWordInfo> suggestions)259     private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
260             final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) {
261         final SuggestedWordInfo typedWordInfo = suggestions.get(0);
262         typedWordInfo.setDebugString("+");
263         final int suggestionsSize = suggestions.size();
264         final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(suggestionsSize);
265         suggestionsList.add(typedWordInfo);
266         // Note: i here is the index in mScores[], but the index in mSuggestions is one more
267         // than i because we added the typed word to mSuggestions without touching mScores.
268         for (int i = 0; i < suggestionsSize - 1; ++i) {
269             final SuggestedWordInfo cur = suggestions.get(i + 1);
270             final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
271                     typedWord, cur.toString(), cur.mScore);
272             final String scoreInfoString;
273             if (normalizedScore > 0) {
274                 scoreInfoString = String.format(
275                         Locale.ROOT, "%d (%4.2f), %s", cur.mScore, normalizedScore,
276                         cur.mSourceDict.mDictType);
277             } else {
278                 scoreInfoString = Integer.toString(cur.mScore);
279             }
280             cur.setDebugString(scoreInfoString);
281             suggestionsList.add(cur);
282         }
283         return suggestionsList;
284     }
285 
getTransformedSuggestedWordInfo( final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase, final boolean isOnlyFirstCharCapitalized, final int trailingSingleQuotesCount)286     /* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo(
287             final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase,
288             final boolean isOnlyFirstCharCapitalized, final int trailingSingleQuotesCount) {
289         final StringBuilder sb = new StringBuilder(wordInfo.mWord.length());
290         if (isAllUpperCase) {
291             sb.append(wordInfo.mWord.toUpperCase(locale));
292         } else if (isOnlyFirstCharCapitalized) {
293             sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale));
294         } else {
295             sb.append(wordInfo.mWord);
296         }
297         // Appending quotes is here to help people quote words. However, it's not helpful
298         // when they type words with quotes toward the end like "it's" or "didn't", where
299         // it's more likely the user missed the last character (or didn't type it yet).
300         final int quotesToAppend = trailingSingleQuotesCount
301                 - (-1 == wordInfo.mWord.indexOf(Constants.CODE_SINGLE_QUOTE) ? 0 : 1);
302         for (int i = quotesToAppend - 1; i >= 0; --i) {
303             sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE);
304         }
305         return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKindAndFlags,
306                 wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord,
307                 wordInfo.mAutoCommitFirstWordConfidence);
308     }
309 }
310