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