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 SuggestedWords(@onnull final ArrayList<SuggestedWordInfo> suggestedWordInfoList, @Nullable final ArrayList<SuggestedWordInfo> rawSuggestions, @Nullable final SuggestedWordInfo typedWordInfo, final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, final int inputStyle, final int sequenceNumber)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 isEmpty()92 public boolean isEmpty() { 93 return mSuggestedWordInfoList.isEmpty(); 94 } 95 size()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 */ getWordCountToShow(final boolean shouldShowLxxSuggestionUi)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 */ getTypedWordInfo()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 */ getWord(final int index)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 */ getLabel(final int index)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 */ getInfo(final int index)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 */ indexOf(SuggestedWordInfo suggestedWordInfo)156 public int indexOf(SuggestedWordInfo suggestedWordInfo) { 157 return mSuggestedWordInfoList.indexOf(suggestedWordInfo); 158 } 159 getDebugString(final int pos)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 */ isPunctuationSuggestions()179 public boolean isPunctuationSuggestions() { 180 return false; 181 } 182 183 @Override toString()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 getFromApplicationSpecifiedCompletions( final CompletionInfo[] infos)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 getEmptyInstance()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. getTypedWordAndPreviousSuggestions( @onnull final SuggestedWordInfo typedWordInfo, @Nonnull final SuggestedWords previousSuggestions)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 getAutoCommitCandidate()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 */ SuggestedWordInfo(final String word, final String prevWordsContext, final int score, final int kindAndFlags, final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord, final int autoCommitFirstWordConfidence)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 */ SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion)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 isEligibleForAutoCommit()326 public boolean isEligibleForAutoCommit() { 327 return (isKindOf(KIND_CORRECTION) && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord); 328 } 329 getKind()330 public int getKind() { 331 return (mKindAndFlags & KIND_MASK_KIND); 332 } 333 isKindOf(final int kind)334 public boolean isKindOf(final int kind) { 335 return getKind() == kind; 336 } 337 isPossiblyOffensive()338 public boolean isPossiblyOffensive() { 339 return (mKindAndFlags & KIND_FLAG_POSSIBLY_OFFENSIVE) != 0; 340 } 341 isExactMatch()342 public boolean isExactMatch() { 343 return (mKindAndFlags & KIND_FLAG_EXACT_MATCH) != 0; 344 } 345 isExactMatchWithIntentionalOmission()346 public boolean isExactMatchWithIntentionalOmission() { 347 return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0; 348 } 349 isAprapreateForAutoCorrection()350 public boolean isAprapreateForAutoCorrection() { 351 return (mKindAndFlags & KIND_FLAG_APPROPRIATE_FOR_AUTO_CORRECTION) != 0; 352 } 353 setDebugString(final String str)354 public void setDebugString(final String str) { 355 if (null == str) throw new NullPointerException("Debug info is null"); 356 mDebugString = str; 357 } 358 getDebugString()359 public String getDebugString() { 360 return mDebugString; 361 } 362 getWord()363 public String getWord() { 364 return mWord; 365 } 366 367 @Deprecated getSourceDictionary()368 public Dictionary getSourceDictionary() { 369 return mSourceDict; 370 } 371 codePointAt(int i)372 public int codePointAt(int i) { 373 return mWord.codePointAt(i); 374 } 375 376 @Override toString()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 */ removeDups( @ullable final String typedWord, @Nonnull final ArrayList<SuggestedWordInfo> candidates)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 removeSuggestedWordInfoFromList( @onnull final String word, @Nonnull final ArrayList<SuggestedWordInfo> candidates, final int startIndexExclusive)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 isPrediction(final int inputStyle)426 private static boolean isPrediction(final int inputStyle) { 427 return INPUT_STYLE_PREDICTION == inputStyle 428 || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle; 429 } 430 isPrediction()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 getTypedWordInfoOrNull()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