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 
SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, final ArrayList<SuggestedWordInfo> rawSuggestions, final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, final int inputStyle)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 
SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, final ArrayList<SuggestedWordInfo> rawSuggestions, final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, final int inputStyle, final int sequenceNumber)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 
SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList, final ArrayList<SuggestedWordInfo> rawSuggestions, final String typedWord, final boolean typedWordValid, final boolean willAutoCorrect, final boolean isObsoleteSuggestions, final int inputStyle, final int sequenceNumber)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 
isEmpty()107     public boolean isEmpty() {
108         return mSuggestedWordInfoList.isEmpty();
109     }
110 
size()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      */
getWord(final int index)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      */
getLabel(final int index)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      */
getInfo(final int index)141     public SuggestedWordInfo getInfo(final int index) {
142         return mSuggestedWordInfoList.get(index);
143     }
144 
getDebugString(final int pos)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      */
isPunctuationSuggestions()164     public boolean isPunctuationSuggestions() {
165         return false;
166     }
167 
168     @Override
toString()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 
getFromApplicationSpecifiedCompletions( final CompletionInfo[] infos)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.
getTypedWordAndPreviousSuggestions( final String typedWord, final SuggestedWords previousSuggestions)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 
getAutoCommitCandidate()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          */
SuggestedWordInfo(final String word, final int score, final int kindAndFlags, final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord, final int autoCommitFirstWordConfidence)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          */
SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion)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 
isEligibleForAutoCommit()300         public boolean isEligibleForAutoCommit() {
301             return (isKindOf(KIND_CORRECTION) && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord);
302         }
303 
getKind()304         public int getKind() {
305             return (mKindAndFlags & KIND_MASK_KIND);
306         }
307 
isKindOf(final int kind)308         public boolean isKindOf(final int kind) {
309             return getKind() == kind;
310         }
311 
isPossiblyOffensive()312         public boolean isPossiblyOffensive() {
313             return (mKindAndFlags & KIND_FLAG_POSSIBLY_OFFENSIVE) != 0;
314         }
315 
isExactMatch()316         public boolean isExactMatch() {
317             return (mKindAndFlags & KIND_FLAG_EXACT_MATCH) != 0;
318         }
319 
isExactMatchWithIntentionalOmission()320         public boolean isExactMatchWithIntentionalOmission() {
321             return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0;
322         }
323 
setDebugString(final String str)324         public void setDebugString(final String str) {
325             if (null == str) throw new NullPointerException("Debug info is null");
326             mDebugString = str;
327         }
328 
getDebugString()329         public String getDebugString() {
330             return mDebugString;
331         }
332 
codePointAt(int i)333         public int codePointAt(int i) {
334             return mWord.codePointAt(i);
335         }
336 
337         @Override
toString()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.
removeDups(final String typedWord, ArrayList<SuggestedWordInfo> candidates)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 
removeSuggestedWordInfoFrom(final String word, final ArrayList<SuggestedWordInfo> candidates, final int startIndexExclusive)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 
isPrediction(final int inputStyle)381     private static boolean isPrediction(final int inputStyle) {
382         return INPUT_STYLE_PREDICTION == inputStyle
383                 || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle;
384     }
385 
isPrediction()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.
getSuggestedWordsExcludingTypedWordForRecorrection()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.
getSuggestedWordsForLastWordOfPhraseGesture()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
getTypedWordInfoOrNull()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