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