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