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