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