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 54 public Suggest(final DictionaryFacilitator dictionaryFacilitator) { 55 mDictionaryFacilitator = dictionaryFacilitator; 56 } 57 58 public Locale getLocale() { 59 return mDictionaryFacilitator.getLocale(); 60 } 61 62 public void setAutoCorrectionThreshold(final float threshold) { 63 mAutoCorrectionThreshold = threshold; 64 } 65 66 public interface OnGetSuggestedWordsCallback { 67 public void onGetSuggestedWords(final SuggestedWords suggestedWords); 68 } 69 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 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 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. 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. 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 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 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