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