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