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