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