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