1 /* 2 * Copyright (C) 2010 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 17 package com.android.inputmethod.latin; 18 19 import android.text.TextUtils; 20 import android.view.inputmethod.CompletionInfo; 21 22 import com.android.inputmethod.annotations.UsedForTesting; 23 import com.android.inputmethod.latin.define.DebugFlags; 24 import com.android.inputmethod.latin.utils.StringUtils; 25 26 import java.util.ArrayList; 27 import java.util.Arrays; 28 import java.util.HashSet; 29 30 public class SuggestedWords { 31 public static final int INDEX_OF_TYPED_WORD = 0; 32 public static final int INDEX_OF_AUTO_CORRECTION = 1; 33 public static final int NOT_A_SEQUENCE_NUMBER = -1; 34 35 public static final int INPUT_STYLE_NONE = 0; 36 public static final int INPUT_STYLE_TYPING = 1; 37 public static final int INPUT_STYLE_UPDATE_BATCH = 2; 38 public static final int INPUT_STYLE_TAIL_BATCH = 3; 39 public static final int INPUT_STYLE_APPLICATION_SPECIFIED = 4; 40 public static final int INPUT_STYLE_RECORRECTION = 5; 41 public static final int INPUT_STYLE_PREDICTION = 6; 42 public static final int INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION = 7; 43 44 // The maximum number of suggestions available. 45 public static final int MAX_SUGGESTIONS = 18; 46 47 private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0); 48 public static final SuggestedWords EMPTY = new SuggestedWords( 49 EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false /* typedWordValid */, 50 false /* willAutoCorrect */, false /* isObsoleteSuggestions */, INPUT_STYLE_NONE); 51 52 public final String mTypedWord; 53 public final boolean mTypedWordValid; 54 // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition 55 // of what this flag means would be "the top suggestion is strong enough to auto-correct", 56 // whether this exactly matches the user entry or not. 57 public final boolean mWillAutoCorrect; 58 public final boolean mIsObsoleteSuggestions; 59 // How the input for these suggested words was done by the user. Must be one of the 60 // INPUT_STYLE_* constants above. 61 public final int mInputStyle; 62 public final int mSequenceNumber; // Sequence number for auto-commit. 63 protected final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList; 64 public final ArrayList<SuggestedWordInfo> mRawSuggestions; 65 SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, final ArrayList<SuggestedWordInfo> rawSuggestions, final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, final int inputStyle)66 public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, 67 final ArrayList<SuggestedWordInfo> rawSuggestions, 68 final boolean typedWordValid, 69 final boolean willAutoCorrect, 70 final boolean isObsoleteSuggestions, 71 final int inputStyle) { 72 this(suggestedWordInfoList, rawSuggestions, typedWordValid, willAutoCorrect, 73 isObsoleteSuggestions, inputStyle, NOT_A_SEQUENCE_NUMBER); 74 } 75 SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, final ArrayList<SuggestedWordInfo> rawSuggestions, final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, final int inputStyle, final int sequenceNumber)76 public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, 77 final ArrayList<SuggestedWordInfo> rawSuggestions, 78 final boolean typedWordValid, 79 final boolean willAutoCorrect, 80 final boolean isObsoleteSuggestions, 81 final int inputStyle, 82 final int sequenceNumber) { 83 this(suggestedWordInfoList, rawSuggestions, 84 (suggestedWordInfoList.isEmpty() || isPrediction(inputStyle)) ? null 85 : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord, 86 typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, sequenceNumber); 87 } 88 SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, final ArrayList<SuggestedWordInfo> rawSuggestions, final String typedWord, final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, final int inputStyle, final int sequenceNumber)89 public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, 90 final ArrayList<SuggestedWordInfo> rawSuggestions, 91 final String typedWord, 92 final boolean typedWordValid, 93 final boolean willAutoCorrect, 94 final boolean isObsoleteSuggestions, 95 final int inputStyle, 96 final int sequenceNumber) { 97 mSuggestedWordInfoList = suggestedWordInfoList; 98 mRawSuggestions = rawSuggestions; 99 mTypedWordValid = typedWordValid; 100 mWillAutoCorrect = willAutoCorrect; 101 mIsObsoleteSuggestions = isObsoleteSuggestions; 102 mInputStyle = inputStyle; 103 mSequenceNumber = sequenceNumber; 104 mTypedWord = typedWord; 105 } 106 isEmpty()107 public boolean isEmpty() { 108 return mSuggestedWordInfoList.isEmpty(); 109 } 110 size()111 public int size() { 112 return mSuggestedWordInfoList.size(); 113 } 114 115 /** 116 * Get suggested word at <code>index</code>. 117 * @param index The index of the suggested word. 118 * @return The suggested word. 119 */ getWord(final int index)120 public String getWord(final int index) { 121 return mSuggestedWordInfoList.get(index).mWord; 122 } 123 124 /** 125 * Get displayed text at <code>index</code>. 126 * In RTL languages, the displayed text on the suggestion strip may be different from the 127 * suggested word that is returned from {@link #getWord(int)}. For example the displayed text 128 * of punctuation suggestion "(" should be ")". 129 * @param index The index of the text to display. 130 * @return The text to be displayed. 131 */ getLabel(final int index)132 public String getLabel(final int index) { 133 return mSuggestedWordInfoList.get(index).mWord; 134 } 135 136 /** 137 * Get {@link SuggestedWordInfo} object at <code>index</code>. 138 * @param index The index of the {@link SuggestedWordInfo}. 139 * @return The {@link SuggestedWordInfo} object. 140 */ getInfo(final int index)141 public SuggestedWordInfo getInfo(final int index) { 142 return mSuggestedWordInfoList.get(index); 143 } 144 getDebugString(final int pos)145 public String getDebugString(final int pos) { 146 if (!DebugFlags.DEBUG_ENABLED) { 147 return null; 148 } 149 final SuggestedWordInfo wordInfo = getInfo(pos); 150 if (wordInfo == null) { 151 return null; 152 } 153 final String debugString = wordInfo.getDebugString(); 154 if (TextUtils.isEmpty(debugString)) { 155 return null; 156 } 157 return debugString; 158 } 159 160 /** 161 * The predicator to tell whether this object represents punctuation suggestions. 162 * @return false if this object desn't represent punctuation suggestions. 163 */ isPunctuationSuggestions()164 public boolean isPunctuationSuggestions() { 165 return false; 166 } 167 168 @Override toString()169 public String toString() { 170 // Pretty-print method to help debug 171 return "SuggestedWords:" 172 + " mTypedWordValid=" + mTypedWordValid 173 + " mWillAutoCorrect=" + mWillAutoCorrect 174 + " mInputStyle=" + mInputStyle 175 + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray()); 176 } 177 getFromApplicationSpecifiedCompletions( final CompletionInfo[] infos)178 public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions( 179 final CompletionInfo[] infos) { 180 final ArrayList<SuggestedWordInfo> result = new ArrayList<>(); 181 for (final CompletionInfo info : infos) { 182 if (null == info || null == info.getText()) { 183 continue; 184 } 185 result.add(new SuggestedWordInfo(info)); 186 } 187 return result; 188 } 189 190 // Should get rid of the first one (what the user typed previously) from suggestions 191 // and replace it with what the user currently typed. getTypedWordAndPreviousSuggestions( final String typedWord, final SuggestedWords previousSuggestions)192 public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions( 193 final String typedWord, final SuggestedWords previousSuggestions) { 194 final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(); 195 final HashSet<String> alreadySeen = new HashSet<>(); 196 suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE, 197 SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, 198 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 199 SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); 200 alreadySeen.add(typedWord.toString()); 201 final int previousSize = previousSuggestions.size(); 202 for (int index = 1; index < previousSize; index++) { 203 final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(index); 204 final String prevWord = prevWordInfo.mWord; 205 // Filter out duplicate suggestions. 206 if (!alreadySeen.contains(prevWord)) { 207 suggestionsList.add(prevWordInfo); 208 alreadySeen.add(prevWord); 209 } 210 } 211 return suggestionsList; 212 } 213 getAutoCommitCandidate()214 public SuggestedWordInfo getAutoCommitCandidate() { 215 if (mSuggestedWordInfoList.size() <= 0) return null; 216 final SuggestedWordInfo candidate = mSuggestedWordInfoList.get(0); 217 return candidate.isEligibleForAutoCommit() ? candidate : null; 218 } 219 220 public static final class SuggestedWordInfo { 221 public static final int NOT_AN_INDEX = -1; 222 public static final int NOT_A_CONFIDENCE = -1; 223 public static final int MAX_SCORE = Integer.MAX_VALUE; 224 225 private static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind 226 public static final int KIND_TYPED = 0; // What user typed 227 public static final int KIND_CORRECTION = 1; // Simple correction/suggestion 228 public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars) 229 public static final int KIND_WHITELIST = 3; // Whitelisted word 230 public static final int KIND_BLACKLIST = 4; // Blacklisted word 231 public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation 232 public static final int KIND_APP_DEFINED = 6; // Suggested by the application 233 public static final int KIND_SHORTCUT = 7; // A shortcut 234 public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input) 235 // KIND_RESUMED: A resumed suggestion (comes from a span, currently this type is used only 236 // in java for re-correction) 237 public static final int KIND_RESUMED = 9; 238 public static final int KIND_OOV_CORRECTION = 10; // Most probable string correction 239 240 public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000; 241 public static final int KIND_FLAG_EXACT_MATCH = 0x40000000; 242 public static final int KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION = 0x20000000; 243 244 public final String mWord; 245 // The completion info from the application. Null for suggestions that don't come from 246 // the application (including keyboard-computed ones, so this is almost always null) 247 public final CompletionInfo mApplicationSpecifiedCompletionInfo; 248 public final int mScore; 249 public final int mKindAndFlags; 250 public final int mCodePointCount; 251 public final Dictionary mSourceDict; 252 // For auto-commit. This keeps track of the index inside the touch coordinates array 253 // passed to native code to get suggestions for a gesture that corresponds to the first 254 // letter of the second word. 255 public final int mIndexOfTouchPointOfSecondWord; 256 // For auto-commit. This is a measure of how confident we are that we can commit the 257 // first word of this suggestion. 258 public final int mAutoCommitFirstWordConfidence; 259 private String mDebugString = ""; 260 261 /** 262 * Create a new suggested word info. 263 * @param word The string to suggest. 264 * @param score A measure of how likely this suggestion is. 265 * @param kindAndFlags The kind of suggestion, as one of the above KIND_* constants with 266 * flags. 267 * @param sourceDict What instance of Dictionary produced this suggestion. 268 * @param indexOfTouchPointOfSecondWord See mIndexOfTouchPointOfSecondWord. 269 * @param autoCommitFirstWordConfidence See mAutoCommitFirstWordConfidence. 270 */ SuggestedWordInfo(final String word, final int score, final int kindAndFlags, final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord, final int autoCommitFirstWordConfidence)271 public SuggestedWordInfo(final String word, final int score, final int kindAndFlags, 272 final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord, 273 final int autoCommitFirstWordConfidence) { 274 mWord = word; 275 mApplicationSpecifiedCompletionInfo = null; 276 mScore = score; 277 mKindAndFlags = kindAndFlags; 278 mSourceDict = sourceDict; 279 mCodePointCount = StringUtils.codePointCount(mWord); 280 mIndexOfTouchPointOfSecondWord = indexOfTouchPointOfSecondWord; 281 mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence; 282 } 283 284 /** 285 * Create a new suggested word info from an application-specified completion. 286 * If the passed argument or its contained text is null, this throws a NPE. 287 * @param applicationSpecifiedCompletion The application-specified completion info. 288 */ SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion)289 public SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion) { 290 mWord = applicationSpecifiedCompletion.getText().toString(); 291 mApplicationSpecifiedCompletionInfo = applicationSpecifiedCompletion; 292 mScore = SuggestedWordInfo.MAX_SCORE; 293 mKindAndFlags = SuggestedWordInfo.KIND_APP_DEFINED; 294 mSourceDict = Dictionary.DICTIONARY_APPLICATION_DEFINED; 295 mCodePointCount = StringUtils.codePointCount(mWord); 296 mIndexOfTouchPointOfSecondWord = SuggestedWordInfo.NOT_AN_INDEX; 297 mAutoCommitFirstWordConfidence = SuggestedWordInfo.NOT_A_CONFIDENCE; 298 } 299 isEligibleForAutoCommit()300 public boolean isEligibleForAutoCommit() { 301 return (isKindOf(KIND_CORRECTION) && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord); 302 } 303 getKind()304 public int getKind() { 305 return (mKindAndFlags & KIND_MASK_KIND); 306 } 307 isKindOf(final int kind)308 public boolean isKindOf(final int kind) { 309 return getKind() == kind; 310 } 311 isPossiblyOffensive()312 public boolean isPossiblyOffensive() { 313 return (mKindAndFlags & KIND_FLAG_POSSIBLY_OFFENSIVE) != 0; 314 } 315 isExactMatch()316 public boolean isExactMatch() { 317 return (mKindAndFlags & KIND_FLAG_EXACT_MATCH) != 0; 318 } 319 isExactMatchWithIntentionalOmission()320 public boolean isExactMatchWithIntentionalOmission() { 321 return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0; 322 } 323 setDebugString(final String str)324 public void setDebugString(final String str) { 325 if (null == str) throw new NullPointerException("Debug info is null"); 326 mDebugString = str; 327 } 328 getDebugString()329 public String getDebugString() { 330 return mDebugString; 331 } 332 codePointAt(int i)333 public int codePointAt(int i) { 334 return mWord.codePointAt(i); 335 } 336 337 @Override toString()338 public String toString() { 339 if (TextUtils.isEmpty(mDebugString)) { 340 return mWord; 341 } else { 342 return mWord + " (" + mDebugString + ")"; 343 } 344 } 345 346 // This will always remove the higher index if a duplicate is found. removeDups(final String typedWord, ArrayList<SuggestedWordInfo> candidates)347 public static boolean removeDups(final String typedWord, 348 ArrayList<SuggestedWordInfo> candidates) { 349 if (candidates.isEmpty()) { 350 return false; 351 } 352 final boolean didRemoveTypedWord; 353 if (!TextUtils.isEmpty(typedWord)) { 354 didRemoveTypedWord = removeSuggestedWordInfoFrom(typedWord, candidates, 355 -1 /* startIndexExclusive */); 356 } else { 357 didRemoveTypedWord = false; 358 } 359 for (int i = 0; i < candidates.size(); ++i) { 360 removeSuggestedWordInfoFrom(candidates.get(i).mWord, candidates, 361 i /* startIndexExclusive */); 362 } 363 return didRemoveTypedWord; 364 } 365 removeSuggestedWordInfoFrom(final String word, final ArrayList<SuggestedWordInfo> candidates, final int startIndexExclusive)366 private static boolean removeSuggestedWordInfoFrom(final String word, 367 final ArrayList<SuggestedWordInfo> candidates, final int startIndexExclusive) { 368 boolean didRemove = false; 369 for (int i = startIndexExclusive + 1; i < candidates.size(); ++i) { 370 final SuggestedWordInfo previous = candidates.get(i); 371 if (word.equals(previous.mWord)) { 372 didRemove = true; 373 candidates.remove(i); 374 --i; 375 } 376 } 377 return didRemove; 378 } 379 } 380 isPrediction(final int inputStyle)381 private static boolean isPrediction(final int inputStyle) { 382 return INPUT_STYLE_PREDICTION == inputStyle 383 || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle; 384 } 385 isPrediction()386 public boolean isPrediction() { 387 return isPrediction(mInputStyle); 388 } 389 390 // SuggestedWords is an immutable object, as much as possible. We must not just remove 391 // words from the member ArrayList as some other parties may expect the object to never change. 392 // This is only ever called by recorrection at the moment, hence the ForRecorrection moniker. getSuggestedWordsExcludingTypedWordForRecorrection()393 public SuggestedWords getSuggestedWordsExcludingTypedWordForRecorrection() { 394 final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>(); 395 String typedWord = null; 396 for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { 397 final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); 398 if (!info.isKindOf(SuggestedWordInfo.KIND_TYPED)) { 399 newSuggestions.add(info); 400 } else { 401 assert(null == typedWord); 402 typedWord = info.mWord; 403 } 404 } 405 // We should never autocorrect, so we say the typed word is valid. Also, in this case, 406 // no auto-correction should take place hence willAutoCorrect = false. 407 return new SuggestedWords(newSuggestions, null /* rawSuggestions */, typedWord, 408 true /* typedWordValid */, false /* willAutoCorrect */, mIsObsoleteSuggestions, 409 SuggestedWords.INPUT_STYLE_RECORRECTION, NOT_A_SEQUENCE_NUMBER); 410 } 411 412 // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the 413 // last word of all suggestions, separated by a space. This is necessary because when we commit 414 // a multiple-word suggestion, the IME only retains the last word as the composing word, and 415 // we should only suggest replacements for this last word. 416 // TODO: make this work with languages without spaces. getSuggestedWordsForLastWordOfPhraseGesture()417 public SuggestedWords getSuggestedWordsForLastWordOfPhraseGesture() { 418 final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>(); 419 for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) { 420 final SuggestedWordInfo info = mSuggestedWordInfoList.get(i); 421 final int indexOfLastSpace = info.mWord.lastIndexOf(Constants.CODE_SPACE) + 1; 422 final String lastWord = info.mWord.substring(indexOfLastSpace); 423 newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKindAndFlags, 424 info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX, 425 SuggestedWordInfo.NOT_A_CONFIDENCE)); 426 } 427 return new SuggestedWords(newSuggestions, null /* rawSuggestions */, mTypedWordValid, 428 mWillAutoCorrect, mIsObsoleteSuggestions, INPUT_STYLE_TAIL_BATCH); 429 } 430 431 /** 432 * @return the {@link SuggestedWordInfo} which corresponds to the word that is originally 433 * typed by the user. Otherwise returns {@code null}. Note that gesture input is not 434 * considered to be a typed word. 435 */ 436 @UsedForTesting getTypedWordInfoOrNull()437 public SuggestedWordInfo getTypedWordInfoOrNull() { 438 if (SuggestedWords.INDEX_OF_TYPED_WORD >= size()) { 439 return null; 440 } 441 final SuggestedWordInfo info = getInfo(SuggestedWords.INDEX_OF_TYPED_WORD); 442 return (info.getKind() == SuggestedWordInfo.KIND_TYPED) ? info : null; 443 } 444 } 445