summaryrefslogtreecommitdiffstats
path: root/java/src/com/android/inputmethod/latin/SuggestedWords.java
blob: 390b311e219b4fb5bb7d0a8ee049263da6840b76 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.inputmethod.latin;

import android.text.TextUtils;
import android.view.inputmethod.CompletionInfo;

import com.android.inputmethod.annotations.UsedForTesting;
import com.android.inputmethod.latin.common.Constants;
import com.android.inputmethod.latin.common.StringUtils;
import com.android.inputmethod.latin.define.DebugFlags;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

public class SuggestedWords {
    public static final int INDEX_OF_TYPED_WORD = 0;
    public static final int INDEX_OF_AUTO_CORRECTION = 1;
    public static final int NOT_A_SEQUENCE_NUMBER = -1;

    public static final int INPUT_STYLE_NONE = 0;
    public static final int INPUT_STYLE_TYPING = 1;
    public static final int INPUT_STYLE_UPDATE_BATCH = 2;
    public static final int INPUT_STYLE_TAIL_BATCH = 3;
    public static final int INPUT_STYLE_APPLICATION_SPECIFIED = 4;
    public static final int INPUT_STYLE_RECORRECTION = 5;
    public static final int INPUT_STYLE_PREDICTION = 6;
    public static final int INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION = 7;

    // The maximum number of suggestions available.
    public static final int MAX_SUGGESTIONS = 18;

    private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0);
    @Nonnull
    private static final SuggestedWords EMPTY = new SuggestedWords(
            EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, null /* typedWord */,
            false /* typedWordValid */, false /* willAutoCorrect */,
            false /* isObsoleteSuggestions */, INPUT_STYLE_NONE, NOT_A_SEQUENCE_NUMBER);

    @Nullable
    public final SuggestedWordInfo mTypedWordInfo;
    public final boolean mTypedWordValid;
    // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition
    // of what this flag means would be "the top suggestion is strong enough to auto-correct",
    // whether this exactly matches the user entry or not.
    public final boolean mWillAutoCorrect;
    public final boolean mIsObsoleteSuggestions;
    // How the input for these suggested words was done by the user. Must be one of the
    // INPUT_STYLE_* constants above.
    public final int mInputStyle;
    public final int mSequenceNumber; // Sequence number for auto-commit.
    @Nonnull
    protected final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList;
    @Nullable
    public final ArrayList<SuggestedWordInfo> mRawSuggestions;

    public SuggestedWords(@Nonnull 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) {
        mSuggestedWordInfoList = suggestedWordInfoList;
        mRawSuggestions = rawSuggestions;
        mTypedWordValid = typedWordValid;
        mWillAutoCorrect = willAutoCorrect;
        mIsObsoleteSuggestions = isObsoleteSuggestions;
        mInputStyle = inputStyle;
        mSequenceNumber = sequenceNumber;
        mTypedWordInfo = typedWordInfo;
    }

    public boolean isEmpty() {
        return mSuggestedWordInfoList.isEmpty();
    }

    public int size() {
        return mSuggestedWordInfoList.size();
    }

    /**
     * Get suggested word to show as suggestions to UI.
     *
     * @param shouldShowLxxSuggestionUi true if showing suggestion UI introduced in LXX and later.
     * @return the count of suggested word to show as suggestions to UI.
     */
    public int getWordCountToShow(final boolean shouldShowLxxSuggestionUi) {
        if (isPrediction() || !shouldShowLxxSuggestionUi) {
            return size();
        }
        return size() - /* typed word */ 1;
    }

    /**
     * Get suggested word at <code>index</code>.
     * @param index The index of the suggested word.
     * @return The suggested word.
     */
    public String getWord(final int index) {
        return mSuggestedWordInfoList.get(index).mWord;
    }

    /**
     * Get displayed text at <code>index</code>.
     * In RTL languages, the displayed text on the suggestion strip may be different from the
     * suggested word that is returned from {@link #getWord(int)}. For example the displayed text
     * of punctuation suggestion "(" should be ")".
     * @param index The index of the text to display.
     * @return The text to be displayed.
     */
    public String getLabel(final int index) {
        return mSuggestedWordInfoList.get(index).mWord;
    }

    /**
     * Get {@link SuggestedWordInfo} object at <code>index</code>.
     * @param index The index of the {@link SuggestedWordInfo}.
     * @return The {@link SuggestedWordInfo} object.
     */
    public SuggestedWordInfo getInfo(final int index) {
        return mSuggestedWordInfoList.get(index);
    }

    /**
     * Gets the suggestion index from the suggestions list.
     * @param suggestedWordInfo The {@link SuggestedWordInfo} to find the index.
     * @return The position of the suggestion in the suggestion list.
     */
    public int indexOf(SuggestedWordInfo suggestedWordInfo) {
        return mSuggestedWordInfoList.indexOf(suggestedWordInfo);
    }

    public String getDebugString(final int pos) {
        if (!DebugFlags.DEBUG_ENABLED) {
            return null;
        }
        final SuggestedWordInfo wordInfo = getInfo(pos);
        if (wordInfo == null) {
            return null;
        }
        final String debugString = wordInfo.getDebugString();
        if (TextUtils.isEmpty(debugString)) {
            return null;
        }
        return debugString;
    }

    /**
     * The predicator to tell whether this object represents punctuation suggestions.
     * @return false if this object desn't represent punctuation suggestions.
     */
    public boolean isPunctuationSuggestions() {
        return false;
    }

    @Override
    public String toString() {
        // Pretty-print method to help debug
        return "SuggestedWords:"
                + " mTypedWordValid=" + mTypedWordValid
                + " mWillAutoCorrect=" + mWillAutoCorrect
                + " mInputStyle=" + mInputStyle
                + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray());
    }

    public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions(
            final CompletionInfo[] infos) {
        final ArrayList<SuggestedWordInfo> result = new ArrayList<>();
        for (final CompletionInfo info : infos) {
            if (null == info || null == info.getText()) {
                continue;
            }
            result.add(new SuggestedWordInfo(info));
        }
        return result;
    }

    @Nonnull
    public static final SuggestedWords getEmptyInstance() {
        return SuggestedWords.EMPTY;
    }

    // Should get rid of the first one (what the user typed previously) from suggestions
    // and replace it with what the user currently typed.
    public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions(
            @Nonnull final SuggestedWordInfo typedWordInfo,
            @Nonnull final SuggestedWords previousSuggestions) {
        final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>();
        final HashSet<String> alreadySeen = new HashSet<>();
        suggestionsList.add(typedWordInfo);
        alreadySeen.add(typedWordInfo.mWord);
        final int previousSize = previousSuggestions.size();
        for (int index = 1; index < previousSize; index++) {
            final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(index);
            final String prevWord = prevWordInfo.mWord;
            // Filter out duplicate suggestions.
            if (!alreadySeen.contains(prevWord)) {
                suggestionsList.add(prevWordInfo);
                alreadySeen.add(prevWord);
            }
        }
        return suggestionsList;
    }

    public SuggestedWordInfo getAutoCommitCandidate() {
        if (mSuggestedWordInfoList.size() <= 0) return null;
        final SuggestedWordInfo candidate = mSuggestedWordInfoList.get(0);
        return candidate.isEligibleForAutoCommit() ? candidate : null;
    }

    // non-final for testability.
    public static class SuggestedWordInfo {
        public static final int NOT_AN_INDEX = -1;
        public static final int NOT_A_CONFIDENCE = -1;
        public static final int MAX_SCORE = Integer.MAX_VALUE;

        private static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind
        public static final int KIND_TYPED = 0; // What user typed
        public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
        public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
        public static final int KIND_WHITELIST = 3; // Whitelisted word
        public static final int KIND_BLACKLIST = 4; // Blacklisted word
        public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation
        public static final int KIND_APP_DEFINED = 6; // Suggested by the application
        public static final int KIND_SHORTCUT = 7; // A shortcut
        public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
        // KIND_RESUMED: A resumed suggestion (comes from a span, currently this type is used only
        // in java for re-correction)
        public static final int KIND_RESUMED = 9;
        public static final int KIND_OOV_CORRECTION = 10; // Most probable string correction

        public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
        public static final int KIND_FLAG_EXACT_MATCH = 0x40000000;
        public static final int KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION = 0x20000000;

        public final String mWord;
        // The completion info from the application. Null for suggestions that don't come from
        // the application (including keyboard-computed ones, so this is almost always null)
        public final CompletionInfo mApplicationSpecifiedCompletionInfo;
        public final int mScore;
        public final int mKindAndFlags;
        public final int mCodePointCount;
        public final Dictionary mSourceDict;
        // For auto-commit. This keeps track of the index inside the touch coordinates array
        // passed to native code to get suggestions for a gesture that corresponds to the first
        // letter of the second word.
        public final int mIndexOfTouchPointOfSecondWord;
        // For auto-commit. This is a measure of how confident we are that we can commit the
        // first word of this suggestion.
        public final int mAutoCommitFirstWordConfidence;
        private String mDebugString = "";

        /**
         * Create a new suggested word info.
         * @param word The string to suggest.
         * @param score A measure of how likely this suggestion is.
         * @param kindAndFlags The kind of suggestion, as one of the above KIND_* constants with
         * flags.
         * @param sourceDict What instance of Dictionary produced this suggestion.
         * @param indexOfTouchPointOfSecondWord See mIndexOfTouchPointOfSecondWord.
         * @param autoCommitFirstWordConfidence See mAutoCommitFirstWordConfidence.
         */
        public SuggestedWordInfo(final String word, final int score, final int kindAndFlags,
                final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord,
                final int autoCommitFirstWordConfidence) {
            mWord = word;
            mApplicationSpecifiedCompletionInfo = null;
            mScore = score;
            mKindAndFlags = kindAndFlags;
            mSourceDict = sourceDict;
            mCodePointCount = StringUtils.codePointCount(mWord);
            mIndexOfTouchPointOfSecondWord = indexOfTouchPointOfSecondWord;
            mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence;
        }

        /**
         * Create a new suggested word info from an application-specified completion.
         * If the passed argument or its contained text is null, this throws a NPE.
         * @param applicationSpecifiedCompletion The application-specified completion info.
         */
        public SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion) {
            mWord = applicationSpecifiedCompletion.getText().toString();
            mApplicationSpecifiedCompletionInfo = applicationSpecifiedCompletion;
            mScore = SuggestedWordInfo.MAX_SCORE;
            mKindAndFlags = SuggestedWordInfo.KIND_APP_DEFINED;
            mSourceDict = Dictionary.DICTIONARY_APPLICATION_DEFINED;
            mCodePointCount = StringUtils.codePointCount(mWord);
            mIndexOfTouchPointOfSecondWord = SuggestedWordInfo.NOT_AN_INDEX;
            mAutoCommitFirstWordConfidence = SuggestedWordInfo.NOT_A_CONFIDENCE;
        }

        public boolean isEligibleForAutoCommit() {
            return (isKindOf(KIND_CORRECTION) && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord);
        }

        public int getKind() {
            return (mKindAndFlags & KIND_MASK_KIND);
        }

        public boolean isKindOf(final int kind) {
            return getKind() == kind;
        }

        public boolean isPossiblyOffensive() {
            return (mKindAndFlags & KIND_FLAG_POSSIBLY_OFFENSIVE) != 0;
        }

        public boolean isExactMatch() {
            return (mKindAndFlags & KIND_FLAG_EXACT_MATCH) != 0;
        }

        public boolean isExactMatchWithIntentionalOmission() {
            return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0;
        }

        public void setDebugString(final String str) {
            if (null == str) throw new NullPointerException("Debug info is null");
            mDebugString = str;
        }

        public String getDebugString() {
            return mDebugString;
        }

        public int codePointAt(int i) {
            return mWord.codePointAt(i);
        }

        @Override
        public String toString() {
            if (TextUtils.isEmpty(mDebugString)) {
                return mWord;
            }
            return mWord + " (" + mDebugString + ")";
        }

        // This will always remove the higher index if a duplicate is found.
        public static void removeDups(@Nullable final String typedWord,
                @Nonnull ArrayList<SuggestedWordInfo> candidates) {
            if (candidates.isEmpty()) {
                return;
            }
            if (!TextUtils.isEmpty(typedWord)) {
                removeSuggestedWordInfoFromList(typedWord, candidates, -1 /* startIndexExclusive */);
            }
            for (int i = 0; i < candidates.size(); ++i) {
                removeSuggestedWordInfoFromList(candidates.get(i).mWord, candidates,
                        i /* startIndexExclusive */);
            }
        }

        private static void removeSuggestedWordInfoFromList(
                @Nonnull final String word, @Nonnull final ArrayList<SuggestedWordInfo> candidates,
                final int startIndexExclusive) {
            for (int i = startIndexExclusive + 1; i < candidates.size(); ++i) {
                final SuggestedWordInfo previous = candidates.get(i);
                if (word.equals(previous.mWord)) {
                    candidates.remove(i);
                    --i;
                }
            }
        }
    }

    private static boolean isPrediction(final int inputStyle) {
        return INPUT_STYLE_PREDICTION == inputStyle
                || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle;
    }

    public boolean isPrediction() {
        return isPrediction(mInputStyle);
    }

    // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the
    // last word of all suggestions, separated by a space. This is necessary because when we commit
    // a multiple-word suggestion, the IME only retains the last word as the composing word, and
    // we should only suggest replacements for this last word.
    // TODO: make this work with languages without spaces.
    public SuggestedWords getSuggestedWordsForLastWordOfPhraseGesture() {
        final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>();
        for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
            final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
            final int indexOfLastSpace = info.mWord.lastIndexOf(Constants.CODE_SPACE) + 1;
            final String lastWord = info.mWord.substring(indexOfLastSpace);
            newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKindAndFlags,
                    info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX,
                    SuggestedWordInfo.NOT_A_CONFIDENCE));
        }
        return new SuggestedWords(newSuggestions, null /* rawSuggestions */,
                newSuggestions.isEmpty() ? null : newSuggestions.get(0) /* typedWordInfo */,
                mTypedWordValid, mWillAutoCorrect, mIsObsoleteSuggestions, INPUT_STYLE_TAIL_BATCH,
                NOT_A_SEQUENCE_NUMBER);
    }

    /**
     * @return the {@link SuggestedWordInfo} which corresponds to the word that is originally
     * typed by the user. Otherwise returns {@code null}. Note that gesture input is not
     * considered to be a typed word.
     */
    @UsedForTesting
    public SuggestedWordInfo getTypedWordInfoOrNull() {
        if (SuggestedWords.INDEX_OF_TYPED_WORD >= size()) {
            return null;
        }
        final SuggestedWordInfo info = getInfo(SuggestedWords.INDEX_OF_TYPED_WORD);
        return (info.getKind() == SuggestedWordInfo.KIND_TYPED) ? info : null;
    }
}