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