Suggest.java revision ee5c032557ab3629babbacc1e52f1a6d1cd8d844
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    // Retrieves suggestions for the typing input
91    // and calls the callback function with the suggestions.
92    private void getSuggestedWordsForTypingInput(final WordComposer wordComposer,
93            final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
94            final boolean blockOffensiveWords, final boolean isCorrectionEnabled,
95            final int[] additionalFeaturesOptions, final int sequenceNumber,
96            final OnGetSuggestedWordsCallback callback) {
97        final String typedWord = wordComposer.getTypedWord();
98        final int trailingSingleQuotesCount = StringUtils.getTrailingSingleQuotesCount(typedWord);
99        final String consideredWord = trailingSingleQuotesCount > 0
100                ? typedWord.substring(0, typedWord.length() - trailingSingleQuotesCount)
101                : typedWord;
102
103        final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
104                wordComposer, prevWordsInfo, proximityInfo, blockOffensiveWords,
105                additionalFeaturesOptions, SESSION_TYPING);
106
107        final boolean isPrediction = !wordComposer.isComposingWord();
108        final boolean shouldMakeSuggestionsAllUpperCase = wordComposer.isAllUpperCase()
109                && !wordComposer.isResumed();
110        final boolean isOnlyFirstCharCapitalized =
111                wordComposer.isOrWillBeOnlyFirstCharCapitalized();
112
113        final ArrayList<SuggestedWordInfo> suggestionsContainer =
114                new ArrayList<>(suggestionResults);
115        final int suggestionsCount = suggestionsContainer.size();
116        if (isOnlyFirstCharCapitalized || shouldMakeSuggestionsAllUpperCase
117                || 0 != trailingSingleQuotesCount) {
118            for (int i = 0; i < suggestionsCount; ++i) {
119                final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
120                final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
121                        wordInfo, suggestionResults.mLocale, shouldMakeSuggestionsAllUpperCase,
122                        isOnlyFirstCharCapitalized, trailingSingleQuotesCount);
123                suggestionsContainer.set(i, transformedWordInfo);
124            }
125        }
126        final boolean didRemoveTypedWord =
127                SuggestedWordInfo.removeDups(typedWord, suggestionsContainer);
128
129        final SuggestedWordInfo firstSuggestedWordInfo;
130        final String whitelistedWord;
131        if (suggestionsContainer.isEmpty()) {
132            firstSuggestedWordInfo = null;
133            whitelistedWord = null;
134        } else {
135            firstSuggestedWordInfo = suggestionsContainer.get(0);
136            if (!firstSuggestedWordInfo.isKindOf(SuggestedWordInfo.KIND_WHITELIST)) {
137                whitelistedWord = null;
138            } else {
139                whitelistedWord = firstSuggestedWordInfo.mWord;
140            }
141        }
142
143        // We allow auto-correction if we have a whitelisted word, or if the word had more than
144        // one char and was not suggested.
145        final boolean allowsToBeAutoCorrected = (null != whitelistedWord)
146                || (consideredWord.length() > 1 && !didRemoveTypedWord);
147
148        final boolean hasAutoCorrection;
149        // TODO: using isCorrectionEnabled here is not very good. It's probably useless, because
150        // any attempt to do auto-correction is already shielded with a test for this flag; at the
151        // same time, it feels wrong that the SuggestedWord object includes information about
152        // the current settings. It may also be useful to know, when the setting is off, whether
153        // the word *would* have been auto-corrected.
154        if (!isCorrectionEnabled || !allowsToBeAutoCorrected || isPrediction
155                || null == firstSuggestedWordInfo || wordComposer.hasDigits()
156                || wordComposer.isMostlyCaps() || wordComposer.isResumed()
157                || !mDictionaryFacilitator.hasInitializedMainDictionary()
158                || firstSuggestedWordInfo.isKindOf(SuggestedWordInfo.KIND_SHORTCUT)) {
159            // If we don't have a main dictionary, we never want to auto-correct. The reason for
160            // this is, the user may have a contact whose name happens to match a valid word in
161            // their language, and it will unexpectedly auto-correct. For example, if the user
162            // types in English with no dictionary and has a "Will" in their contact list, "will"
163            // would always auto-correct to "Will" which is unwanted. Hence, no main dict => no
164            // auto-correct.
165            // Also, shortcuts should never auto-correct unless they are whitelist entries.
166            // TODO: we may want to have shortcut-only entries auto-correct in the future.
167            hasAutoCorrection = false;
168        } else {
169            hasAutoCorrection = AutoCorrectionUtils.suggestionExceedsAutoCorrectionThreshold(
170                    firstSuggestedWordInfo, consideredWord, mAutoCorrectionThreshold);
171        }
172
173        if (!TextUtils.isEmpty(typedWord)) {
174            suggestionsContainer.add(0, new SuggestedWordInfo(typedWord,
175                    SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_TYPED,
176                    Dictionary.DICTIONARY_USER_TYPED,
177                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
178                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
179        }
180
181        final ArrayList<SuggestedWordInfo> suggestionsList;
182        if (DBG && !suggestionsContainer.isEmpty()) {
183            suggestionsList = getSuggestionsInfoListWithDebugInfo(typedWord, suggestionsContainer);
184        } else {
185            suggestionsList = suggestionsContainer;
186        }
187
188        callback.onGetSuggestedWords(new SuggestedWords(suggestionsList,
189                suggestionResults.mRawSuggestions,
190                // TODO: this first argument is lying. If this is a whitelisted word which is an
191                // actual word, it says typedWordValid = false, which looks wrong. We should either
192                // rename the attribute or change the value.
193                !isPrediction && !allowsToBeAutoCorrected /* typedWordValid */,
194                hasAutoCorrection /* willAutoCorrect */,
195                false /* isObsoleteSuggestions */, isPrediction, sequenceNumber));
196    }
197
198    // Retrieves suggestions for the batch input
199    // and calls the callback function with the suggestions.
200    private void getSuggestedWordsForBatchInput(final WordComposer wordComposer,
201            final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
202            final boolean blockOffensiveWords, final int[] additionalFeaturesOptions,
203            final int sessionId, final int sequenceNumber,
204            final OnGetSuggestedWordsCallback callback) {
205        final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
206                wordComposer, prevWordsInfo, proximityInfo, blockOffensiveWords,
207                additionalFeaturesOptions, sessionId);
208        final ArrayList<SuggestedWordInfo> suggestionsContainer =
209                new ArrayList<>(suggestionResults);
210        final int suggestionsCount = suggestionsContainer.size();
211        final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock();
212        final boolean isAllUpperCase = wordComposer.isAllUpperCase();
213        if (isFirstCharCapitalized || isAllUpperCase) {
214            for (int i = 0; i < suggestionsCount; ++i) {
215                final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
216                final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
217                        wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized,
218                        0 /* trailingSingleQuotesCount */);
219                suggestionsContainer.set(i, transformedWordInfo);
220            }
221        }
222
223        if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord,
224                wordComposer.getRejectedBatchModeSuggestion())) {
225            final SuggestedWordInfo rejected = suggestionsContainer.remove(0);
226            suggestionsContainer.add(1, rejected);
227        }
228        SuggestedWordInfo.removeDups(null /* typedWord */, suggestionsContainer);
229
230        // For some reason some suggestions with MIN_VALUE are making their way here.
231        // TODO: Find a more robust way to detect distractors.
232        for (int i = suggestionsContainer.size() - 1; i >= 0; --i) {
233            if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) {
234                suggestionsContainer.remove(i);
235            }
236        }
237
238        // In the batch input mode, the most relevant suggested word should act as a "typed word"
239        // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
240        callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer,
241                suggestionResults.mRawSuggestions,
242                true /* typedWordValid */,
243                false /* willAutoCorrect */,
244                false /* isObsoleteSuggestions */,
245                false /* isPrediction */, sequenceNumber));
246    }
247
248    private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
249            final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) {
250        final SuggestedWordInfo typedWordInfo = suggestions.get(0);
251        typedWordInfo.setDebugString("+");
252        final int suggestionsSize = suggestions.size();
253        final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(suggestionsSize);
254        suggestionsList.add(typedWordInfo);
255        // Note: i here is the index in mScores[], but the index in mSuggestions is one more
256        // than i because we added the typed word to mSuggestions without touching mScores.
257        for (int i = 0; i < suggestionsSize - 1; ++i) {
258            final SuggestedWordInfo cur = suggestions.get(i + 1);
259            final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
260                    typedWord, cur.toString(), cur.mScore);
261            final String scoreInfoString;
262            if (normalizedScore > 0) {
263                scoreInfoString = String.format(
264                        Locale.ROOT, "%d (%4.2f), %s", cur.mScore, normalizedScore,
265                        cur.mSourceDict.mDictType);
266            } else {
267                scoreInfoString = Integer.toString(cur.mScore);
268            }
269            cur.setDebugString(scoreInfoString);
270            suggestionsList.add(cur);
271        }
272        return suggestionsList;
273    }
274
275    /* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo(
276            final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase,
277            final boolean isOnlyFirstCharCapitalized, final int trailingSingleQuotesCount) {
278        final StringBuilder sb = new StringBuilder(wordInfo.mWord.length());
279        if (isAllUpperCase) {
280            sb.append(wordInfo.mWord.toUpperCase(locale));
281        } else if (isOnlyFirstCharCapitalized) {
282            sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale));
283        } else {
284            sb.append(wordInfo.mWord);
285        }
286        // Appending quotes is here to help people quote words. However, it's not helpful
287        // when they type words with quotes toward the end like "it's" or "didn't", where
288        // it's more likely the user missed the last character (or didn't type it yet).
289        final int quotesToAppend = trailingSingleQuotesCount
290                - (-1 == wordInfo.mWord.indexOf(Constants.CODE_SINGLE_QUOTE) ? 0 : 1);
291        for (int i = quotesToAppend - 1; i >= 0; --i) {
292            sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE);
293        }
294        return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKindAndFlags,
295                wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord,
296                wordInfo.mAutoCommitFirstWordConfidence);
297    }
298}
299