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