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