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