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