Home | History | Annotate | Download | only in spellcheck
      1 /*
      2  * Copyright (C) 2012 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.spellcheck;
     18 
     19 import android.annotation.TargetApi;
     20 import android.content.res.Resources;
     21 import android.os.Binder;
     22 import android.os.Build;
     23 import android.text.TextUtils;
     24 import android.util.Log;
     25 import android.view.textservice.SentenceSuggestionsInfo;
     26 import android.view.textservice.SuggestionsInfo;
     27 import android.view.textservice.TextInfo;
     28 
     29 import com.android.inputmethod.compat.TextInfoCompatUtils;
     30 import com.android.inputmethod.latin.NgramContext;
     31 import com.android.inputmethod.latin.utils.SpannableStringUtils;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Locale;
     35 
     36 public final class AndroidSpellCheckerSession extends AndroidWordLevelSpellCheckerSession {
     37     private static final String TAG = AndroidSpellCheckerSession.class.getSimpleName();
     38     private static final boolean DBG = false;
     39     private final Resources mResources;
     40     private SentenceLevelAdapter mSentenceLevelAdapter;
     41 
     42     public AndroidSpellCheckerSession(AndroidSpellCheckerService service) {
     43         super(service);
     44         mResources = service.getResources();
     45     }
     46 
     47     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
     48     private SentenceSuggestionsInfo fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti,
     49             SentenceSuggestionsInfo ssi) {
     50         final CharSequence typedText = TextInfoCompatUtils.getCharSequenceOrString(ti);
     51         if (!typedText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
     52             return null;
     53         }
     54         final int N = ssi.getSuggestionsCount();
     55         final ArrayList<Integer> additionalOffsets = new ArrayList<>();
     56         final ArrayList<Integer> additionalLengths = new ArrayList<>();
     57         final ArrayList<SuggestionsInfo> additionalSuggestionsInfos = new ArrayList<>();
     58         CharSequence currentWord = null;
     59         for (int i = 0; i < N; ++i) {
     60             final SuggestionsInfo si = ssi.getSuggestionsInfoAt(i);
     61             final int flags = si.getSuggestionsAttributes();
     62             if ((flags & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) == 0) {
     63                 continue;
     64             }
     65             final int offset = ssi.getOffsetAt(i);
     66             final int length = ssi.getLengthAt(i);
     67             final CharSequence subText = typedText.subSequence(offset, offset + length);
     68             final NgramContext ngramContext =
     69                     new NgramContext(new NgramContext.WordInfo(currentWord));
     70             currentWord = subText;
     71             if (!subText.toString().contains(AndroidSpellCheckerService.SINGLE_QUOTE)) {
     72                 continue;
     73             }
     74             // Split preserving spans.
     75             final CharSequence[] splitTexts = SpannableStringUtils.split(subText,
     76                     AndroidSpellCheckerService.SINGLE_QUOTE,
     77                     true /* preserveTrailingEmptySegments */);
     78             if (splitTexts == null || splitTexts.length <= 1) {
     79                 continue;
     80             }
     81             final int splitNum = splitTexts.length;
     82             for (int j = 0; j < splitNum; ++j) {
     83                 final CharSequence splitText = splitTexts[j];
     84                 if (TextUtils.isEmpty(splitText)) {
     85                     continue;
     86                 }
     87                 if (mSuggestionsCache.getSuggestionsFromCache(splitText.toString()) == null) {
     88                     continue;
     89                 }
     90                 final int newLength = splitText.length();
     91                 // Neither RESULT_ATTR_IN_THE_DICTIONARY nor RESULT_ATTR_LOOKS_LIKE_TYPO
     92                 final int newFlags = 0;
     93                 final SuggestionsInfo newSi = new SuggestionsInfo(newFlags, EMPTY_STRING_ARRAY);
     94                 newSi.setCookieAndSequence(si.getCookie(), si.getSequence());
     95                 if (DBG) {
     96                     Log.d(TAG, "Override and remove old span over: " + splitText + ", "
     97                             + offset + "," + newLength);
     98                 }
     99                 additionalOffsets.add(offset);
    100                 additionalLengths.add(newLength);
    101                 additionalSuggestionsInfos.add(newSi);
    102             }
    103         }
    104         final int additionalSize = additionalOffsets.size();
    105         if (additionalSize <= 0) {
    106             return null;
    107         }
    108         final int suggestionsSize = N + additionalSize;
    109         final int[] newOffsets = new int[suggestionsSize];
    110         final int[] newLengths = new int[suggestionsSize];
    111         final SuggestionsInfo[] newSuggestionsInfos = new SuggestionsInfo[suggestionsSize];
    112         int i;
    113         for (i = 0; i < N; ++i) {
    114             newOffsets[i] = ssi.getOffsetAt(i);
    115             newLengths[i] = ssi.getLengthAt(i);
    116             newSuggestionsInfos[i] = ssi.getSuggestionsInfoAt(i);
    117         }
    118         for (; i < suggestionsSize; ++i) {
    119             newOffsets[i] = additionalOffsets.get(i - N);
    120             newLengths[i] = additionalLengths.get(i - N);
    121             newSuggestionsInfos[i] = additionalSuggestionsInfos.get(i - N);
    122         }
    123         return new SentenceSuggestionsInfo(newSuggestionsInfos, newOffsets, newLengths);
    124     }
    125 
    126     @Override
    127     public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos,
    128             int suggestionsLimit) {
    129         final SentenceSuggestionsInfo[] retval = splitAndSuggest(textInfos, suggestionsLimit);
    130         if (retval == null || retval.length != textInfos.length) {
    131             return retval;
    132         }
    133         for (int i = 0; i < retval.length; ++i) {
    134             final SentenceSuggestionsInfo tempSsi =
    135                     fixWronglyInvalidatedWordWithSingleQuote(textInfos[i], retval[i]);
    136             if (tempSsi != null) {
    137                 retval[i] = tempSsi;
    138             }
    139         }
    140         return retval;
    141     }
    142 
    143     /**
    144      * Get sentence suggestions for specified texts in an array of TextInfo. This is taken from
    145      * SpellCheckerService#onGetSentenceSuggestionsMultiple that we can't use because it's
    146      * using private variables.
    147      * The default implementation splits the input text to words and returns
    148      * {@link SentenceSuggestionsInfo} which contains suggestions for each word.
    149      * This function will run on the incoming IPC thread.
    150      * So, this is not called on the main thread,
    151      * but will be called in series on another thread.
    152      * @param textInfos an array of the text metadata
    153      * @param suggestionsLimit the maximum number of suggestions to be returned
    154      * @return an array of {@link SentenceSuggestionsInfo} returned by
    155      * {@link android.service.textservice.SpellCheckerService.Session#onGetSuggestions(TextInfo, int)}
    156      */
    157     private SentenceSuggestionsInfo[] splitAndSuggest(TextInfo[] textInfos, int suggestionsLimit) {
    158         if (textInfos == null || textInfos.length == 0) {
    159             return SentenceLevelAdapter.getEmptySentenceSuggestionsInfo();
    160         }
    161         SentenceLevelAdapter sentenceLevelAdapter;
    162         synchronized(this) {
    163             sentenceLevelAdapter = mSentenceLevelAdapter;
    164             if (sentenceLevelAdapter == null) {
    165                 final String localeStr = getLocale();
    166                 if (!TextUtils.isEmpty(localeStr)) {
    167                     sentenceLevelAdapter = new SentenceLevelAdapter(mResources,
    168                             new Locale(localeStr));
    169                     mSentenceLevelAdapter = sentenceLevelAdapter;
    170                 }
    171             }
    172         }
    173         if (sentenceLevelAdapter == null) {
    174             return SentenceLevelAdapter.getEmptySentenceSuggestionsInfo();
    175         }
    176         final int infosSize = textInfos.length;
    177         final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize];
    178         for (int i = 0; i < infosSize; ++i) {
    179             final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams =
    180                     sentenceLevelAdapter.getSplitWords(textInfos[i]);
    181             final ArrayList<SentenceLevelAdapter.SentenceWordItem> mItems =
    182                     textInfoParams.mItems;
    183             final int itemsSize = mItems.size();
    184             final TextInfo[] splitTextInfos = new TextInfo[itemsSize];
    185             for (int j = 0; j < itemsSize; ++j) {
    186                 splitTextInfos[j] = mItems.get(j).mTextInfo;
    187             }
    188             retval[i] = SentenceLevelAdapter.reconstructSuggestions(
    189                     textInfoParams, onGetSuggestionsMultiple(
    190                             splitTextInfos, suggestionsLimit, true));
    191         }
    192         return retval;
    193     }
    194 
    195     @Override
    196     public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos,
    197             int suggestionsLimit, boolean sequentialWords) {
    198         long ident = Binder.clearCallingIdentity();
    199         try {
    200             final int length = textInfos.length;
    201             final SuggestionsInfo[] retval = new SuggestionsInfo[length];
    202             for (int i = 0; i < length; ++i) {
    203                 final CharSequence prevWord;
    204                 if (sequentialWords && i > 0) {
    205                     final TextInfo prevTextInfo = textInfos[i - 1];
    206                     final CharSequence prevWordCandidate =
    207                             TextInfoCompatUtils.getCharSequenceOrString(prevTextInfo);
    208                     // Note that an empty string would be used to indicate the initial word
    209                     // in the future.
    210                     prevWord = TextUtils.isEmpty(prevWordCandidate) ? null : prevWordCandidate;
    211                 } else {
    212                     prevWord = null;
    213                 }
    214                 final NgramContext ngramContext =
    215                         new NgramContext(new NgramContext.WordInfo(prevWord));
    216                 final TextInfo textInfo = textInfos[i];
    217                 retval[i] = onGetSuggestionsInternal(textInfo, ngramContext, suggestionsLimit);
    218                 retval[i].setCookieAndSequence(textInfo.getCookie(), textInfo.getSequence());
    219             }
    220             return retval;
    221         } finally {
    222             Binder.restoreCallingIdentity(ident);
    223         }
    224     }
    225 }
    226