Home | History | Annotate | Download | only in latin
      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 
     17 package com.android.inputmethod.latin;
     18 
     19 import android.text.TextUtils;
     20 
     21 import com.android.inputmethod.keyboard.ProximityInfo;
     22 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
     23 import com.android.inputmethod.latin.define.DebugFlags;
     24 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
     25 import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
     26 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
     27 import com.android.inputmethod.latin.utils.StringUtils;
     28 import com.android.inputmethod.latin.utils.SuggestionResults;
     29 
     30 import java.util.ArrayList;
     31 import 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  */
     37 public 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;
    190         if (resultsArePredictions) {
    191             inputStyle = suggestionResults.mIsBeginningOfSentence
    192                     ? SuggestedWords.INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION
    193                     : SuggestedWords.INPUT_STYLE_PREDICTION;
    194         } else {
    195             inputStyle = inputStyleIfNotPrediction;
    196         }
    197         callback.onGetSuggestedWords(new SuggestedWords(suggestionsList,
    198                 suggestionResults.mRawSuggestions,
    199                 // TODO: this first argument is lying. If this is a whitelisted word which is an
    200                 // actual word, it says typedWordValid = false, which looks wrong. We should either
    201                 // rename the attribute or change the value.
    202                 !resultsArePredictions && !allowsToBeAutoCorrected /* typedWordValid */,
    203                 hasAutoCorrection /* willAutoCorrect */,
    204                 false /* isObsoleteSuggestions */, inputStyle, sequenceNumber));
    205     }
    206 
    207     // Retrieves suggestions for the batch input
    208     // and calls the callback function with the suggestions.
    209     private void getSuggestedWordsForBatchInput(final WordComposer wordComposer,
    210             final PrevWordsInfo prevWordsInfo, final ProximityInfo proximityInfo,
    211             final SettingsValuesForSuggestion settingsValuesForSuggestion,
    212             final int inputStyle, final int sequenceNumber,
    213             final OnGetSuggestedWordsCallback callback) {
    214         final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
    215                 wordComposer, prevWordsInfo, proximityInfo, settingsValuesForSuggestion,
    216                 SESSION_ID_GESTURE);
    217         final ArrayList<SuggestedWordInfo> suggestionsContainer =
    218                 new ArrayList<>(suggestionResults);
    219         final int suggestionsCount = suggestionsContainer.size();
    220         final boolean isFirstCharCapitalized = wordComposer.wasShiftedNoLock();
    221         final boolean isAllUpperCase = wordComposer.isAllUpperCase();
    222         if (isFirstCharCapitalized || isAllUpperCase) {
    223             for (int i = 0; i < suggestionsCount; ++i) {
    224                 final SuggestedWordInfo wordInfo = suggestionsContainer.get(i);
    225                 final SuggestedWordInfo transformedWordInfo = getTransformedSuggestedWordInfo(
    226                         wordInfo, suggestionResults.mLocale, isAllUpperCase, isFirstCharCapitalized,
    227                         0 /* trailingSingleQuotesCount */);
    228                 suggestionsContainer.set(i, transformedWordInfo);
    229             }
    230         }
    231 
    232         if (suggestionsContainer.size() > 1 && TextUtils.equals(suggestionsContainer.get(0).mWord,
    233                 wordComposer.getRejectedBatchModeSuggestion())) {
    234             final SuggestedWordInfo rejected = suggestionsContainer.remove(0);
    235             suggestionsContainer.add(1, rejected);
    236         }
    237         SuggestedWordInfo.removeDups(null /* typedWord */, suggestionsContainer);
    238 
    239         // For some reason some suggestions with MIN_VALUE are making their way here.
    240         // TODO: Find a more robust way to detect distractors.
    241         for (int i = suggestionsContainer.size() - 1; i >= 0; --i) {
    242             if (suggestionsContainer.get(i).mScore < SUPPRESS_SUGGEST_THRESHOLD) {
    243                 suggestionsContainer.remove(i);
    244             }
    245         }
    246 
    247         // In the batch input mode, the most relevant suggested word should act as a "typed word"
    248         // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
    249         // Note that because this method is never used to get predictions, there is no need to
    250         // modify inputType such in getSuggestedWordsForNonBatchInput.
    251         callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer,
    252                 suggestionResults.mRawSuggestions,
    253                 true /* typedWordValid */,
    254                 false /* willAutoCorrect */,
    255                 false /* isObsoleteSuggestions */,
    256                 inputStyle, sequenceNumber));
    257     }
    258 
    259     private static ArrayList<SuggestedWordInfo> getSuggestionsInfoListWithDebugInfo(
    260             final String typedWord, final ArrayList<SuggestedWordInfo> suggestions) {
    261         final SuggestedWordInfo typedWordInfo = suggestions.get(0);
    262         typedWordInfo.setDebugString("+");
    263         final int suggestionsSize = suggestions.size();
    264         final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>(suggestionsSize);
    265         suggestionsList.add(typedWordInfo);
    266         // Note: i here is the index in mScores[], but the index in mSuggestions is one more
    267         // than i because we added the typed word to mSuggestions without touching mScores.
    268         for (int i = 0; i < suggestionsSize - 1; ++i) {
    269             final SuggestedWordInfo cur = suggestions.get(i + 1);
    270             final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
    271                     typedWord, cur.toString(), cur.mScore);
    272             final String scoreInfoString;
    273             if (normalizedScore > 0) {
    274                 scoreInfoString = String.format(
    275                         Locale.ROOT, "%d (%4.2f), %s", cur.mScore, normalizedScore,
    276                         cur.mSourceDict.mDictType);
    277             } else {
    278                 scoreInfoString = Integer.toString(cur.mScore);
    279             }
    280             cur.setDebugString(scoreInfoString);
    281             suggestionsList.add(cur);
    282         }
    283         return suggestionsList;
    284     }
    285 
    286     /* package for test */ static SuggestedWordInfo getTransformedSuggestedWordInfo(
    287             final SuggestedWordInfo wordInfo, final Locale locale, final boolean isAllUpperCase,
    288             final boolean isOnlyFirstCharCapitalized, final int trailingSingleQuotesCount) {
    289         final StringBuilder sb = new StringBuilder(wordInfo.mWord.length());
    290         if (isAllUpperCase) {
    291             sb.append(wordInfo.mWord.toUpperCase(locale));
    292         } else if (isOnlyFirstCharCapitalized) {
    293             sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale));
    294         } else {
    295             sb.append(wordInfo.mWord);
    296         }
    297         // Appending quotes is here to help people quote words. However, it's not helpful
    298         // when they type words with quotes toward the end like "it's" or "didn't", where
    299         // it's more likely the user missed the last character (or didn't type it yet).
    300         final int quotesToAppend = trailingSingleQuotesCount
    301                 - (-1 == wordInfo.mWord.indexOf(Constants.CODE_SINGLE_QUOTE) ? 0 : 1);
    302         for (int i = quotesToAppend - 1; i >= 0; --i) {
    303             sb.appendCodePoint(Constants.CODE_SINGLE_QUOTE);
    304         }
    305         return new SuggestedWordInfo(sb.toString(), wordInfo.mScore, wordInfo.mKindAndFlags,
    306                 wordInfo.mSourceDict, wordInfo.mIndexOfTouchPointOfSecondWord,
    307                 wordInfo.mAutoCommitFirstWordConfidence);
    308     }
    309 }
    310