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