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    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
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
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
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
107    public boolean isEmpty() {
108        return mSuggestedWordInfoList.isEmpty();
109    }
110
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     */
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     */
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     */
141    public SuggestedWordInfo getInfo(final int index) {
142        return mSuggestedWordInfoList.get(index);
143    }
144
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     */
164    public boolean isPunctuationSuggestions() {
165        return false;
166    }
167
168    @Override
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
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.
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
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         */
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         */
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
300        public boolean isEligibleForAutoCommit() {
301            return (isKindOf(KIND_CORRECTION) && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord);
302        }
303
304        public int getKind() {
305            return (mKindAndFlags & KIND_MASK_KIND);
306        }
307
308        public boolean isKindOf(final int kind) {
309            return getKind() == kind;
310        }
311
312        public boolean isPossiblyOffensive() {
313            return (mKindAndFlags & KIND_FLAG_POSSIBLY_OFFENSIVE) != 0;
314        }
315
316        public boolean isExactMatch() {
317            return (mKindAndFlags & KIND_FLAG_EXACT_MATCH) != 0;
318        }
319
320        public boolean isExactMatchWithIntentionalOmission() {
321            return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0;
322        }
323
324        public void setDebugString(final String str) {
325            if (null == str) throw new NullPointerException("Debug info is null");
326            mDebugString = str;
327        }
328
329        public String getDebugString() {
330            return mDebugString;
331        }
332
333        public int codePointAt(int i) {
334            return mWord.codePointAt(i);
335        }
336
337        @Override
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.
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
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
381    private static boolean isPrediction(final int inputStyle) {
382        return INPUT_STYLE_PREDICTION == inputStyle
383                || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle;
384    }
385
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.
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.
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
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