Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2010 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 import android.view.inputmethod.CompletionInfo;
     21 
     22 import com.android.inputmethod.annotations.UsedForTesting;
     23 import com.android.inputmethod.latin.define.DebugFlags;
     24 import com.android.inputmethod.latin.utils.StringUtils;
     25 
     26 import java.util.ArrayList;
     27 import java.util.Arrays;
     28 import java.util.HashSet;
     29 
     30 public class SuggestedWords {
     31     public static final int INDEX_OF_TYPED_WORD = 0;
     32     public static final int INDEX_OF_AUTO_CORRECTION = 1;
     33     public static final int NOT_A_SEQUENCE_NUMBER = -1;
     34 
     35     public static final int INPUT_STYLE_NONE = 0;
     36     public static final int INPUT_STYLE_TYPING = 1;
     37     public static final int INPUT_STYLE_UPDATE_BATCH = 2;
     38     public static final int INPUT_STYLE_TAIL_BATCH = 3;
     39     public static final int INPUT_STYLE_APPLICATION_SPECIFIED = 4;
     40     public static final int INPUT_STYLE_RECORRECTION = 5;
     41     public static final int INPUT_STYLE_PREDICTION = 6;
     42     public static final int INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION = 7;
     43 
     44     // The maximum number of suggestions available.
     45     public static final int MAX_SUGGESTIONS = 18;
     46 
     47     private static final ArrayList<SuggestedWordInfo> EMPTY_WORD_INFO_LIST = new ArrayList<>(0);
     48     public static final SuggestedWords EMPTY = new SuggestedWords(
     49             EMPTY_WORD_INFO_LIST, null /* rawSuggestions */, false /* typedWordValid */,
     50             false /* willAutoCorrect */, false /* isObsoleteSuggestions */, INPUT_STYLE_NONE);
     51 
     52     public final String mTypedWord;
     53     public final boolean mTypedWordValid;
     54     // Note: this INCLUDES cases where the word will auto-correct to itself. A good definition
     55     // of what this flag means would be "the top suggestion is strong enough to auto-correct",
     56     // whether this exactly matches the user entry or not.
     57     public final boolean mWillAutoCorrect;
     58     public final boolean mIsObsoleteSuggestions;
     59     // How the input for these suggested words was done by the user. Must be one of the
     60     // INPUT_STYLE_* constants above.
     61     public final int mInputStyle;
     62     public final int mSequenceNumber; // Sequence number for auto-commit.
     63     protected final ArrayList<SuggestedWordInfo> mSuggestedWordInfoList;
     64     public final ArrayList<SuggestedWordInfo> mRawSuggestions;
     65 
     66     public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
     67             final ArrayList<SuggestedWordInfo> rawSuggestions,
     68             final boolean typedWordValid,
     69             final boolean willAutoCorrect,
     70             final boolean isObsoleteSuggestions,
     71             final int inputStyle) {
     72         this(suggestedWordInfoList, rawSuggestions, typedWordValid, willAutoCorrect,
     73                 isObsoleteSuggestions, inputStyle, NOT_A_SEQUENCE_NUMBER);
     74     }
     75 
     76     public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
     77             final ArrayList<SuggestedWordInfo> rawSuggestions,
     78             final boolean typedWordValid,
     79             final boolean willAutoCorrect,
     80             final boolean isObsoleteSuggestions,
     81             final int inputStyle,
     82             final int sequenceNumber) {
     83         this(suggestedWordInfoList, rawSuggestions,
     84                 (suggestedWordInfoList.isEmpty() || isPrediction(inputStyle)) ? null
     85                         : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord,
     86                 typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, sequenceNumber);
     87     }
     88 
     89     public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
     90             final ArrayList<SuggestedWordInfo> rawSuggestions,
     91             final String typedWord,
     92             final boolean typedWordValid,
     93             final boolean willAutoCorrect,
     94             final boolean isObsoleteSuggestions,
     95             final int inputStyle,
     96             final int sequenceNumber) {
     97         mSuggestedWordInfoList = suggestedWordInfoList;
     98         mRawSuggestions = rawSuggestions;
     99         mTypedWordValid = typedWordValid;
    100         mWillAutoCorrect = willAutoCorrect;
    101         mIsObsoleteSuggestions = isObsoleteSuggestions;
    102         mInputStyle = inputStyle;
    103         mSequenceNumber = sequenceNumber;
    104         mTypedWord = typedWord;
    105     }
    106 
    107     public boolean isEmpty() {
    108         return mSuggestedWordInfoList.isEmpty();
    109     }
    110 
    111     public int size() {
    112         return mSuggestedWordInfoList.size();
    113     }
    114 
    115     /**
    116      * Get suggested word at <code>index</code>.
    117      * @param index The index of the suggested word.
    118      * @return The suggested word.
    119      */
    120     public String getWord(final int index) {
    121         return mSuggestedWordInfoList.get(index).mWord;
    122     }
    123 
    124     /**
    125      * Get displayed text at <code>index</code>.
    126      * In RTL languages, the displayed text on the suggestion strip may be different from the
    127      * suggested word that is returned from {@link #getWord(int)}. For example the displayed text
    128      * of punctuation suggestion "(" should be ")".
    129      * @param index The index of the text to display.
    130      * @return The text to be displayed.
    131      */
    132     public String getLabel(final int index) {
    133         return mSuggestedWordInfoList.get(index).mWord;
    134     }
    135 
    136     /**
    137      * Get {@link SuggestedWordInfo} object at <code>index</code>.
    138      * @param index The index of the {@link SuggestedWordInfo}.
    139      * @return The {@link SuggestedWordInfo} object.
    140      */
    141     public SuggestedWordInfo getInfo(final int index) {
    142         return mSuggestedWordInfoList.get(index);
    143     }
    144 
    145     public String getDebugString(final int pos) {
    146         if (!DebugFlags.DEBUG_ENABLED) {
    147             return null;
    148         }
    149         final SuggestedWordInfo wordInfo = getInfo(pos);
    150         if (wordInfo == null) {
    151             return null;
    152         }
    153         final String debugString = wordInfo.getDebugString();
    154         if (TextUtils.isEmpty(debugString)) {
    155             return null;
    156         }
    157         return debugString;
    158     }
    159 
    160     /**
    161      * The predicator to tell whether this object represents punctuation suggestions.
    162      * @return false if this object desn't represent punctuation suggestions.
    163      */
    164     public boolean isPunctuationSuggestions() {
    165         return false;
    166     }
    167 
    168     @Override
    169     public String toString() {
    170         // Pretty-print method to help debug
    171         return "SuggestedWords:"
    172                 + " mTypedWordValid=" + mTypedWordValid
    173                 + " mWillAutoCorrect=" + mWillAutoCorrect
    174                 + " mInputStyle=" + mInputStyle
    175                 + " words=" + Arrays.toString(mSuggestedWordInfoList.toArray());
    176     }
    177 
    178     public static ArrayList<SuggestedWordInfo> getFromApplicationSpecifiedCompletions(
    179             final CompletionInfo[] infos) {
    180         final ArrayList<SuggestedWordInfo> result = new ArrayList<>();
    181         for (final CompletionInfo info : infos) {
    182             if (null == info || null == info.getText()) {
    183                 continue;
    184             }
    185             result.add(new SuggestedWordInfo(info));
    186         }
    187         return result;
    188     }
    189 
    190     // Should get rid of the first one (what the user typed previously) from suggestions
    191     // and replace it with what the user currently typed.
    192     public static ArrayList<SuggestedWordInfo> getTypedWordAndPreviousSuggestions(
    193             final String typedWord, final SuggestedWords previousSuggestions) {
    194         final ArrayList<SuggestedWordInfo> suggestionsList = new ArrayList<>();
    195         final HashSet<String> alreadySeen = new HashSet<>();
    196         suggestionsList.add(new SuggestedWordInfo(typedWord, SuggestedWordInfo.MAX_SCORE,
    197                 SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED,
    198                 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
    199                 SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
    200         alreadySeen.add(typedWord.toString());
    201         final int previousSize = previousSuggestions.size();
    202         for (int index = 1; index < previousSize; index++) {
    203             final SuggestedWordInfo prevWordInfo = previousSuggestions.getInfo(index);
    204             final String prevWord = prevWordInfo.mWord;
    205             // Filter out duplicate suggestions.
    206             if (!alreadySeen.contains(prevWord)) {
    207                 suggestionsList.add(prevWordInfo);
    208                 alreadySeen.add(prevWord);
    209             }
    210         }
    211         return suggestionsList;
    212     }
    213 
    214     public SuggestedWordInfo getAutoCommitCandidate() {
    215         if (mSuggestedWordInfoList.size() <= 0) return null;
    216         final SuggestedWordInfo candidate = mSuggestedWordInfoList.get(0);
    217         return candidate.isEligibleForAutoCommit() ? candidate : null;
    218     }
    219 
    220     public static final class SuggestedWordInfo {
    221         public static final int NOT_AN_INDEX = -1;
    222         public static final int NOT_A_CONFIDENCE = -1;
    223         public static final int MAX_SCORE = Integer.MAX_VALUE;
    224 
    225         private static final int KIND_MASK_KIND = 0xFF; // Mask to get only the kind
    226         public static final int KIND_TYPED = 0; // What user typed
    227         public static final int KIND_CORRECTION = 1; // Simple correction/suggestion
    228         public static final int KIND_COMPLETION = 2; // Completion (suggestion with appended chars)
    229         public static final int KIND_WHITELIST = 3; // Whitelisted word
    230         public static final int KIND_BLACKLIST = 4; // Blacklisted word
    231         public static final int KIND_HARDCODED = 5; // Hardcoded suggestion, e.g. punctuation
    232         public static final int KIND_APP_DEFINED = 6; // Suggested by the application
    233         public static final int KIND_SHORTCUT = 7; // A shortcut
    234         public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
    235         // KIND_RESUMED: A resumed suggestion (comes from a span, currently this type is used only
    236         // in java for re-correction)
    237         public static final int KIND_RESUMED = 9;
    238         public static final int KIND_OOV_CORRECTION = 10; // Most probable string correction
    239 
    240         public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
    241         public static final int KIND_FLAG_EXACT_MATCH = 0x40000000;
    242         public static final int KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION = 0x20000000;
    243 
    244         public final String mWord;
    245         // The completion info from the application. Null for suggestions that don't come from
    246         // the application (including keyboard-computed ones, so this is almost always null)
    247         public final CompletionInfo mApplicationSpecifiedCompletionInfo;
    248         public final int mScore;
    249         public final int mKindAndFlags;
    250         public final int mCodePointCount;
    251         public final Dictionary mSourceDict;
    252         // For auto-commit. This keeps track of the index inside the touch coordinates array
    253         // passed to native code to get suggestions for a gesture that corresponds to the first
    254         // letter of the second word.
    255         public final int mIndexOfTouchPointOfSecondWord;
    256         // For auto-commit. This is a measure of how confident we are that we can commit the
    257         // first word of this suggestion.
    258         public final int mAutoCommitFirstWordConfidence;
    259         private String mDebugString = "";
    260 
    261         /**
    262          * Create a new suggested word info.
    263          * @param word The string to suggest.
    264          * @param score A measure of how likely this suggestion is.
    265          * @param kindAndFlags The kind of suggestion, as one of the above KIND_* constants with
    266          * flags.
    267          * @param sourceDict What instance of Dictionary produced this suggestion.
    268          * @param indexOfTouchPointOfSecondWord See mIndexOfTouchPointOfSecondWord.
    269          * @param autoCommitFirstWordConfidence See mAutoCommitFirstWordConfidence.
    270          */
    271         public SuggestedWordInfo(final String word, final int score, final int kindAndFlags,
    272                 final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord,
    273                 final int autoCommitFirstWordConfidence) {
    274             mWord = word;
    275             mApplicationSpecifiedCompletionInfo = null;
    276             mScore = score;
    277             mKindAndFlags = kindAndFlags;
    278             mSourceDict = sourceDict;
    279             mCodePointCount = StringUtils.codePointCount(mWord);
    280             mIndexOfTouchPointOfSecondWord = indexOfTouchPointOfSecondWord;
    281             mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence;
    282         }
    283 
    284         /**
    285          * Create a new suggested word info from an application-specified completion.
    286          * If the passed argument or its contained text is null, this throws a NPE.
    287          * @param applicationSpecifiedCompletion The application-specified completion info.
    288          */
    289         public SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion) {
    290             mWord = applicationSpecifiedCompletion.getText().toString();
    291             mApplicationSpecifiedCompletionInfo = applicationSpecifiedCompletion;
    292             mScore = SuggestedWordInfo.MAX_SCORE;
    293             mKindAndFlags = SuggestedWordInfo.KIND_APP_DEFINED;
    294             mSourceDict = Dictionary.DICTIONARY_APPLICATION_DEFINED;
    295             mCodePointCount = StringUtils.codePointCount(mWord);
    296             mIndexOfTouchPointOfSecondWord = SuggestedWordInfo.NOT_AN_INDEX;
    297             mAutoCommitFirstWordConfidence = SuggestedWordInfo.NOT_A_CONFIDENCE;
    298         }
    299 
    300         public boolean isEligibleForAutoCommit() {
    301             return (isKindOf(KIND_CORRECTION) && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord);
    302         }
    303 
    304         public int getKind() {
    305             return (mKindAndFlags & KIND_MASK_KIND);
    306         }
    307 
    308         public boolean isKindOf(final int kind) {
    309             return getKind() == kind;
    310         }
    311 
    312         public boolean isPossiblyOffensive() {
    313             return (mKindAndFlags & KIND_FLAG_POSSIBLY_OFFENSIVE) != 0;
    314         }
    315 
    316         public boolean isExactMatch() {
    317             return (mKindAndFlags & KIND_FLAG_EXACT_MATCH) != 0;
    318         }
    319 
    320         public boolean isExactMatchWithIntentionalOmission() {
    321             return (mKindAndFlags & KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) != 0;
    322         }
    323 
    324         public void setDebugString(final String str) {
    325             if (null == str) throw new NullPointerException("Debug info is null");
    326             mDebugString = str;
    327         }
    328 
    329         public String getDebugString() {
    330             return mDebugString;
    331         }
    332 
    333         public int codePointAt(int i) {
    334             return mWord.codePointAt(i);
    335         }
    336 
    337         @Override
    338         public String toString() {
    339             if (TextUtils.isEmpty(mDebugString)) {
    340                 return mWord;
    341             } else {
    342                 return mWord + " (" + mDebugString + ")";
    343             }
    344         }
    345 
    346         // This will always remove the higher index if a duplicate is found.
    347         public static boolean removeDups(final String typedWord,
    348                 ArrayList<SuggestedWordInfo> candidates) {
    349             if (candidates.isEmpty()) {
    350                 return false;
    351             }
    352             final boolean didRemoveTypedWord;
    353             if (!TextUtils.isEmpty(typedWord)) {
    354                 didRemoveTypedWord = removeSuggestedWordInfoFrom(typedWord, candidates,
    355                         -1 /* startIndexExclusive */);
    356             } else {
    357                 didRemoveTypedWord = false;
    358             }
    359             for (int i = 0; i < candidates.size(); ++i) {
    360                 removeSuggestedWordInfoFrom(candidates.get(i).mWord, candidates,
    361                         i /* startIndexExclusive */);
    362             }
    363             return didRemoveTypedWord;
    364         }
    365 
    366         private static boolean removeSuggestedWordInfoFrom(final String word,
    367                 final ArrayList<SuggestedWordInfo> candidates, final int startIndexExclusive) {
    368             boolean didRemove = false;
    369             for (int i = startIndexExclusive + 1; i < candidates.size(); ++i) {
    370                 final SuggestedWordInfo previous = candidates.get(i);
    371                 if (word.equals(previous.mWord)) {
    372                     didRemove = true;
    373                     candidates.remove(i);
    374                     --i;
    375                 }
    376             }
    377             return didRemove;
    378         }
    379     }
    380 
    381     private static boolean isPrediction(final int inputStyle) {
    382         return INPUT_STYLE_PREDICTION == inputStyle
    383                 || INPUT_STYLE_BEGINNING_OF_SENTENCE_PREDICTION == inputStyle;
    384     }
    385 
    386     public boolean isPrediction() {
    387         return isPrediction(mInputStyle);
    388     }
    389 
    390     // SuggestedWords is an immutable object, as much as possible. We must not just remove
    391     // words from the member ArrayList as some other parties may expect the object to never change.
    392     // This is only ever called by recorrection at the moment, hence the ForRecorrection moniker.
    393     public SuggestedWords getSuggestedWordsExcludingTypedWordForRecorrection() {
    394         final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>();
    395         String typedWord = null;
    396         for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
    397             final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
    398             if (!info.isKindOf(SuggestedWordInfo.KIND_TYPED)) {
    399                 newSuggestions.add(info);
    400             } else {
    401                 assert(null == typedWord);
    402                 typedWord = info.mWord;
    403             }
    404         }
    405         // We should never autocorrect, so we say the typed word is valid. Also, in this case,
    406         // no auto-correction should take place hence willAutoCorrect = false.
    407         return new SuggestedWords(newSuggestions, null /* rawSuggestions */, typedWord,
    408                 true /* typedWordValid */, false /* willAutoCorrect */, mIsObsoleteSuggestions,
    409                 SuggestedWords.INPUT_STYLE_RECORRECTION, NOT_A_SEQUENCE_NUMBER);
    410     }
    411 
    412     // Creates a new SuggestedWordInfo from the currently suggested words that removes all but the
    413     // last word of all suggestions, separated by a space. This is necessary because when we commit
    414     // a multiple-word suggestion, the IME only retains the last word as the composing word, and
    415     // we should only suggest replacements for this last word.
    416     // TODO: make this work with languages without spaces.
    417     public SuggestedWords getSuggestedWordsForLastWordOfPhraseGesture() {
    418         final ArrayList<SuggestedWordInfo> newSuggestions = new ArrayList<>();
    419         for (int i = 0; i < mSuggestedWordInfoList.size(); ++i) {
    420             final SuggestedWordInfo info = mSuggestedWordInfoList.get(i);
    421             final int indexOfLastSpace = info.mWord.lastIndexOf(Constants.CODE_SPACE) + 1;
    422             final String lastWord = info.mWord.substring(indexOfLastSpace);
    423             newSuggestions.add(new SuggestedWordInfo(lastWord, info.mScore, info.mKindAndFlags,
    424                     info.mSourceDict, SuggestedWordInfo.NOT_AN_INDEX,
    425                     SuggestedWordInfo.NOT_A_CONFIDENCE));
    426         }
    427         return new SuggestedWords(newSuggestions, null /* rawSuggestions */, mTypedWordValid,
    428                 mWillAutoCorrect, mIsObsoleteSuggestions, INPUT_STYLE_TAIL_BATCH);
    429     }
    430 
    431     /**
    432      * @return the {@link SuggestedWordInfo} which corresponds to the word that is originally
    433      * typed by the user. Otherwise returns {@code null}. Note that gesture input is not
    434      * considered to be a typed word.
    435      */
    436     @UsedForTesting
    437     public SuggestedWordInfo getTypedWordInfoOrNull() {
    438         if (SuggestedWords.INDEX_OF_TYPED_WORD >= size()) {
    439             return null;
    440         }
    441         final SuggestedWordInfo info = getInfo(SuggestedWords.INDEX_OF_TYPED_WORD);
    442         return (info.getKind() == SuggestedWordInfo.KIND_TYPED) ? info : null;
    443     }
    444 }
    445