Suggest.java revision e5a8615de706e47ec0a25022aed4df44f4d4b155
1/*
2 * Copyright (C) 2008 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;
20
21import com.android.inputmethod.keyboard.ProximityInfo;
22import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
23import com.android.inputmethod.latin.define.ProductionFlag;
24import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
25import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
26import com.android.inputmethod.latin.utils.StringUtils;
27import com.android.inputmethod.latin.utils.SuggestionResults;
28
29import java.util.ArrayList;
30import java.util.Locale;
31
32/**
33 * This class loads a dictionary and provides a list of suggestions for a given sequence of
34 * characters. This includes corrections and completions.
35 */
36public final class Suggest {
37    public static final String TAG = Suggest.class.getSimpleName();
38
39    // Session id for
40    // {@link #getSuggestedWords(WordComposer,String,ProximityInfo,boolean,int)}.
41    // We are sharing the same ID between typing and gesture to save RAM footprint.
42    public static final int SESSION_TYPING = 0;
43    public static final int SESSION_GESTURE = 0;
44
45    // TODO: rename this to CORRECTION_OFF
46    public static final int CORRECTION_NONE = 0;
47    // TODO: rename this to CORRECTION_ON
48    public static final int CORRECTION_FULL = 1;
49
50    // Close to -2**31
51    private static final int SUPPRESS_SUGGEST_THRESHOLD = -2000000000;
52
53    private static final boolean DBG = LatinImeLogger.sDBG;
54    private final DictionaryFacilitator mDictionaryFacilitator;
55
56    private float mAutoCorrectionThreshold;
57
58    public Suggest(final DictionaryFacilitator dictionaryFacilitator) {
59        mDictionaryFacilitator = dictionaryFacilitator;
60    }
61
62    public Locale getLocale() {
63        return mDictionaryFacilitator.getLocale();
64    }
65
66    public void setAutoCorrectionThreshold(final float threshold) {
67        mAutoCorrectionThreshold = threshold;
68    }
69
70    public interface OnGetSuggestedWordsCallback {
71        public void onGetSuggestedWords(final SuggestedWords suggestedWords);
72    }
73
74    public void getSuggestedWords(final WordComposer wordComposer,
75            final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
76            final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
77            final int[] additionalFeaturesOptions, final int sessionId, final int sequenceNumber,
78            final OnGetSuggestedWordsCallback callback) {
79        if (wordComposer.isBatchMode()) {
80            getSuggestedWordsForBatchInput(wordComposer, prevWordsInfo, proximityInfo,
81                    blockOffensiveWords, additionalFeaturesOptions, sessionId, sequenceNumber,
82                    callback);
83        } else {
84            getSuggestedWordsForTypingInput(wordComposer, prevWordsInfo, proximityInfo,
85                    blockOffensiveWords, isCorrectionEnabled, additionalFeaturesOptions,
86                    sequenceNumber, callback);
87        }
88    }
89
90    private static ArrayList<SuggestedWordInfo> getTransformedSuggestedWordInfoList(
91            final WordComposer wordComposer, final SuggestionResults results,
92            final int trailingSingleQuotesCount) {
93        final boolean shouldMakeSuggestionsAllUpperCase = wordComposer.isAllUpperCase()
94                && !wordComposer.isResumed();
95        final boolean isOnlyFirstCharCapitalized =
96                wordComposer.isOrWillBeOnlyFirstCharCapitalized();
97
98        final ArrayList<SuggestedWordInfo> suggestionsContainer = new ArrayList<>(results);
99        final int suggestionsCount = suggestionsContainer.size();
100        if (isOnlyFirstCharCapitalized || shouldMakeSuggestionsAllUpperCase
101                || 0 != trailingSingleQuotesCount) {
102            for (int i = 0; i < suggestionsCount; ++i) {
103                final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
104                final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
105                        wordInfo, results.mLocale, shouldMakeSuggestionsAllUpperCase,
106                        isOnlyFirstCharCapitalized, trailingSingleQuotesCount);
107                suggestionsContainer.set(i, transformedWordInfo);
108            }
109        }
110        return suggestionsContainer;
111    }
112
113    private static String getWhitelistedWordOrNull(final ArrayList<SuggestedWordInfo> suggestions) {
114        if (suggestions.isEmpty()) {
115            return null;
116        }
117        final SuggestedWordInfo firstSuggestedWordInfo = suggestions.get(0);
118        if (!firstSuggestedWordInfo.isKindOf(SuggestedWordInfo.KIND_WHITELIST)) {
119            return null;
120        }
121        return firstSuggestedWordInfo.mWord;
122    }
123
124    // Retrieves suggestions for the typing input
125    // and calls the callback function with the suggestions.
126    private void getSuggestedWordsForTypingInput(final WordComposer wordComposer,
127            final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
128            final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
129            final int[] additionalFeaturesOptions, final int sequenceNumber,
130            final OnGetSuggestedWordsCallback callback) {
131        final String typedWord = wordComposer.getTypedWord();
132        final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(typedWord);
133        final String consideredWord = trailingSingleQuotesCount > 0
134                ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount)
135                : typedWord;
136
137        final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
138                wordComposer, prevWordsInfo, proximityInfo, blockOffensiveWords,
139                additionalFeaturesOptions, SESSION_TYPING);
140        final ArrayList<SuggestedWordInfo> suggestionsContainer =
141                getTransformedSuggestedWordInfoList(wordComposer, suggestionResults,
142                        trailingSingleQuotesCount);
143        final boolean didRemoveTypedWord =
144                SuggestedWordInfo.removeDups(wordComposer.getTypedWord(), suggestionsContainer);
145
146        final String whitelistedWord = getWhitelistedWordOrNull(suggestionsContainer);
147        final boolean resultsArePredictions = !wordComposer.isComposingWord();
148
149        // We allow auto-correction if we have a whitelisted word, or if the word had more than
150        // one char and was not suggested.
151        final boolean allowsToBeAutoCorrected = (null != whitelistedWord)
152                || (consideredWord.length() > 1 && !didRemoveTypedWord);
153
154        final boolean hasAutoCorrection;
155        // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because
156        // any attempt to do auto-correction is already shielded with a test for this flag; at the
157        // same time, it feels wrong that the SuggestedWord object includes information about
158        // the current settings. It may also be useful to know, when the setting is off, whether
159        // the word *would* have been auto-corrected.
160        if (!isCorrectionEnabled || !allowsToBeAutoCorrected || resultsArePredictions
161                || suggestionResults.isEmpty() || wordComposer.hasDigits()
162                || wordComposer.isMostlyCaps() || wordComposer.isResumed()
163                || !mDictionaryFacilitator.hasInitializedMainDictionary()
164                || suggestionResults.first().isKindOf(SuggestedWordInfo.KIND_SHORTCUT)) {
165            // If we don't have a main dictionary, we never want to auto-correct. The reason for
166            // this is, the user may have a contact whose name happens to match a valid word in
167            // their language, and it will unexpectedly auto-correct. For example, if the user
168            // types in English with no dictionary and has a "Will" in their contact list, "will"
169            // would always auto-correct to "Will" which is unwanted. Hence, no main dict => no
170            // auto-correct.
171            // Also, shortcuts should never auto-correct unless they are whitelist entries.
172            // TODO: we may want to have shortcut-only entries auto-correct in the future.
173            hasAutoCorrection = false;
174        } else {
175            hasAutoCorrection = AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold(
176                    suggestionResults.first(), consideredWord, mAutoCorrectionThreshold);
177        }
178
179        if (!TextUtils.isEmpty(typedWord)) {
180            suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
181                    SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
182                    Dictionary.DICTIONARY_USER_TYPED,
183                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
184                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
185        }
186
187        final ArrayList<SuggestedWordInfo> suggestionsList;
188        if (DBG && !suggestionsContainer.isEmpty()) {
189            suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, suggestionsContainer);
190        } else {
191            suggestionsList = suggestionsContainer;
192        }
193
194        callback.onGetSuggestedWords(new SuggestedWords(suggestionsList,
195                suggestionResults.mRawSuggestions,
196                // TODO: this first argument is lying. If this is a whitelisted word which is an
197                // actual word, it says typedWordValid = false, which looks wrong. We should either
198                // rename the attribute or change the value.
199                !resultsArePredictions && !allowsToBeAutoCorrected /* typedWordValid */,
200                hasAutoCorrection /* willAutoCorrect */,
201                false /* isObsoleteSuggestions */, resultsArePredictions, sequenceNumber));
202    }
203
204    // Retrieves suggestions for the batch input
205    // and calls the callback function with the suggestions.
206    private void getSuggestedWordsForBatchInput(final WordComposer wordComposer,
207            final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
208            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
209            final int sessionId, final int sequenceNumber,
210            final OnGetSuggestedWordsCallback callback) {
211        final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
212                wordComposer, prevWordsInfo, proximityInfo, blockOffensiveWords,
213                additionalFeaturesOptions, sessionId);
214        final ArrayList<SuggestedWordInfo> suggestionsContainer =
215                new ArrayList<>(suggestionResults);
216        final int suggestionsCount = suggestionsContainer.size();
217        final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock();
218        final boolean isAllUpperCase = wordComposer.isAllUpperCase();
219        if (isFirstCharCapitalized || isAllUpperCase) {
220            for (int i = 0; i < suggestionsCount; ++i) {
221                final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
222                final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
223                        wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized,
224                        0 /* trailingSingleQuotesCount */);
225                suggestionsContainer.set(i, transformedWordInfo);
226            }
227        }
228
229        if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord,
230                wordComposer.getRejectedBatchModeSuggestion())) {
231            final SuggestedWordInfo rejected = suggestionsContainer.remove(0);
232            suggestionsContainer.add(1, rejected);
233        }
234        SuggestedWordInfo.removeDups(null /* typedWord */, suggestionsContainer);
235
236        // For some reason some suggestions with MIN_VALUE are making their way here.
237        // TODO: Find a more robust way to detect distractors.
238        for (int i = suggestionsContainer.size() - 1; i >= 0; --i) {
239            if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) {
240                suggestionsContainer.remove(i);
241            }
242        }
243
244        // In the batch input mode, the most relevant suggested word should act as a "typed word"
245        // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
246        callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer,
247                suggestionResults.mRawSuggestions,
248                true /* typedWordValid */,
249                false /* willAutoCorrect */,
250                false /* isObsoleteSuggestions */,
251                false /* isPrediction */, sequenceNumber));
252    }
253
254    private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
255            final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) {
256        final SuggestedWordInfo typedWordInfo = suggestions.get(0);
257        typedWordInfo.setDebugString("+");
258        final int suggestionsSize = suggestions.size();
259        final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(suggestionsSize);
260        suggestionsList.add(typedWordInfo);
261        // Note: i here is the index in mScores[], but the index in mSuggestions is one more
262        // than i because we added the typed word to mSuggestions without touching mScores.
263        for (int i = 0; i < suggestionsSize - 1; ++i) {
264            final SuggestedWordInfo cur = suggestions.get(i + 1);
265            final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
266                    typedWord, cur.toString(), cur.mScore);
267            final String scoreInfoString;
268            if (normalizedScore > 0) {
269                scoreInfoString = String.format(
270                        Locale.ROOT, "%d (%4.2f), %s", cur.mScore, normalizedScore,
271                        cur.mSourceDict.mDictType);
272            } else {
273                scoreInfoString = Integer.toString(cur.mScore);
274            }
275            cur.setDebugString(scoreInfoString);
276            suggestionsList.add(cur);
277        }
278        return suggestionsList;
279    }
280
281    /* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo(
282            final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase,
283            final boolean isOnlyFirstCharCapitalized, final int trailingSingleQuotesCount) {
284        final StringBuilder sb = new StringBuilder(wordInfo.mWord.length());
285        if (isAllUpperCase) {
286            sb.append(wordInfo.mWord.toUpperCase(locale));
287        } else if (isOnlyFirstCharCapitalized) {
288            sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale));
289        } else {
290            sb.append(wordInfo.mWord);
291        }
292        // Appending quotes is here to help people quote words. However, it's not helpful
293        // when they type words with quotes toward the end like "it's" or "didn't", where
294        // it's more likely the user missed the last character (or didn't type it yet).
295        final int quotesToAppend = trailingSingleQuotesCount
296                - (-1 == wordInfo.mWord.indexOf(Constants.CODE_SINGLE_QUOTE) ? 0 : 1);
297        for (int i = quotesToAppend - 1; i >= 0; --i) {
298            sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE);
299        }
300        return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKindAndFlags,
301                wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord,
302                wordInfo.mAutoCommitFirstWordConfidence);
303    }
304}
305