Suggest.java revision b8d764772b174cbd37354ffd0009bda56f223dc4
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 17package com.android.inputmethod.latin; 18 19import android.text.TextUtils; 20 21import com.android.inputmethod.keyboard.ProximityInfo; 22import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 23import com.android.inputmethod.latin.define.DebugFlags; 24import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; 25import com.android.inputmethod.latin.utils.AutoCorrectionUtils; 26import com.android.inputmethod.latin.utils.BinaryDictionaryUtils; 27import com.android.inputmethod.latin.utils.StringUtils; 28import com.android.inputmethod.latin.utils.SuggestionResults; 29 30import java.util.ArrayList; 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 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, final int inputStyle, 124 final boolean isCorrectionEnabled, final int sequenceNumber, 125 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 callback.onGetSuggestedWords(new SuggestedWords(suggestionsList, 190 suggestionResults.mRawSuggestions, 191 // TODO: this first argument is lying. If this is a whitelisted word which is an 192 // actual word, it says typedWordValid = false, which looks wrong. We should either 193 // rename the attribute or change the value. 194 !resultsArePredictions && !allowsToBeAutoCorrected /* typedWordValid */, 195 hasAutoCorrection /* willAutoCorrect */, 196 false /* isObsoleteSuggestions */, resultsArePredictions, 197 inputStyle, sequenceNumber)); 198 } 199 200 // Retrieves suggestions for the batch input 201 // and calls the callback function with the suggestions. 202 private void getSuggestedWordsForBatchInput(final WordComposer wordComposer, 203 final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo, 204 final SettingsValuesForSuggestion settingsValuesForSuggestion, 205 final int inputStyle, final int sequenceNumber, 206 final OnGetSuggestedWordsCallback callback) { 207 final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults( 208 wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion, 209 SESSION_ID_GESTURE); 210 final ArrayList<SuggestedWordInfo> suggestionsContainer = 211 new ArrayList<>(suggestionResults); 212 final int suggestionsCount = suggestionsContainer.size(); 213 final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock(); 214 final boolean isAllUpperCase = wordComposer.isAllUpperCase(); 215 if (isFirstCharCapitalized || isAllUpperCase) { 216 for (int i = 0; i < suggestionsCount; ++i) { 217 final SuggestedWordInfo wordInfo = suggestionsContainer.get(i); 218 final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo( 219 wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized, 220 0 /* trailingSingleQuotesCount */); 221 suggestionsContainer.set(i, transformedWordInfo); 222 } 223 } 224 225 if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord, 226 wordComposer.getRejectedBatchModeSuggestion())) { 227 final SuggestedWordInfo rejected = suggestionsContainer.remove(0); 228 suggestionsContainer.add(1, rejected); 229 } 230 SuggestedWordInfo.removeDups(null /* typedWord */, suggestionsContainer); 231 232 // For some reason some suggestions with MIN_VALUE are making their way here. 233 // TODO: Find a more robust way to detect distractors. 234 for (int i = suggestionsContainer.size() - 1; i >= 0; --i) { 235 if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) { 236 suggestionsContainer.remove(i); 237 } 238 } 239 240 // In the batch input mode, the most relevant suggested word should act as a "typed word" 241 // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false). 242 callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer, 243 suggestionResults.mRawSuggestions, 244 true /* typedWordValid */, 245 false /* willAutoCorrect */, 246 false /* isObsoleteSuggestions */, 247 false /* isPrediction */, 248 inputStyle, sequenceNumber)); 249 } 250 251 private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo( 252 final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) { 253 final SuggestedWordInfo typedWordInfo = suggestions.get(0); 254 typedWordInfo.setDebugString("+"); 255 final int suggestionsSize = suggestions.size(); 256 final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(suggestionsSize); 257 suggestionsList.add(typedWordInfo); 258 // Note: i here is the index in mScores[], but the index in mSuggestions is one more 259 // than i because we added the typed word to mSuggestions without touching mScores. 260 for (int i = 0; i < suggestionsSize - 1; ++i) { 261 final SuggestedWordInfo cur = suggestions.get(i + 1); 262 final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore( 263 typedWord, cur.toString(), cur.mScore); 264 final String scoreInfoString; 265 if (normalizedScore > 0) { 266 scoreInfoString = String.format( 267 Locale.ROOT, "%d (%4.2f), %s", cur.mScore, normalizedScore, 268 cur.mSourceDict.mDictType); 269 } else { 270 scoreInfoString = Integer.toString(cur.mScore); 271 } 272 cur.setDebugString(scoreInfoString); 273 suggestionsList.add(cur); 274 } 275 return suggestionsList; 276 } 277 278 /* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo( 279 final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase, 280 final boolean isOnlyFirstCharCapitalized, final int trailingSingleQuotesCount) { 281 final StringBuilder sb = new StringBuilder(wordInfo.mWord.length()); 282 if (isAllUpperCase) { 283 sb.append(wordInfo.mWord.toUpperCase(locale)); 284 } else if (isOnlyFirstCharCapitalized) { 285 sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale)); 286 } else { 287 sb.append(wordInfo.mWord); 288 } 289 // Appending quotes is here to help people quote words. However, it's not helpful 290 // when they type words with quotes toward the end like "it's" or "didn't", where 291 // it's more likely the user missed the last character (or didn't type it yet). 292 final int quotesToAppend = trailingSingleQuotesCount 293 - (-1 == wordInfo.mWord.indexOf(Constants.CODE_SINGLE_QUOTE) ? 0 : 1); 294 for (int i = quotesToAppend - 1; i >= 0; --i) { 295 sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE); 296 } 297 return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKindAndFlags, 298 wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord, 299 wordInfo.mAutoCommitFirstWordConfidence); 300 } 301} 302