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 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 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 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 107 public boolean isEmpty() { 108 return mSuggestedWordInfoList.isEmpty(); 109 } 110 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 */ 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 */ 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 */ 141 public SuggestedWordInfo getInfo(final int index) { 142 return mSuggestedWordInfoList.get(index); 143 } 144 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 */ 164 public boolean isPunctuationSuggestions() { 165 return false; 166 } 167 168 @Override 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 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. 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 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 */ 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 */ 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 300 public boolean isEligibleForAutoCommit() { 301 return (isKindOf(KIND_CORRECTION) && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord); 302 } 303 304 public int getKind() { 305 return (mKindAndFlags & KIND_MASK_KIND); 306 } 307 308 public boolean isKindOf(final int kind) { 309 return getKind() == kind; 310 } 311 312 public boolean isPossiblyOffensive() { 313 return (mKindAndFlags & KIND_FLAG_POSSIBLY_OFFENSIVE) != 0; 314 } 315 316 public boolean isExactMatch() { 317 return (mKindAndFlags & KIND_FLAG_EXACT_MATCH) != 0; 318 } 319 320 public boolean isExactMatchWithIntentionalOmission() { 321 return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0; 322 } 323 324 public void setDebugString(final String str) { 325 if (null == str) throw new NullPointerException("Debug info is null"); 326 mDebugString = str; 327 } 328 329 public String getDebugString() { 330 return mDebugString; 331 } 332 333 public int codePointAt(int i) { 334 return mWord.codePointAt(i); 335 } 336 337 @Override 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. 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 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 381 private static boolean isPrediction(final int inputStyle) { 382 return INPUT_STYLE_PREDICTION == inputStyle 383 || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle; 384 } 385 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. 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. 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 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