AndroidSpellCheckerSession.java revision 86f36003fd4397143bd37938dda029e5707634af
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
17package com.android.inputmethod.latin.spellcheck;
18
19import android.content.res.Resources;
20import android.os.Binder;
21import android.text.TextUtils;
22import android.util.Log;
23import android.view.textservice.SentenceSuggestionsInfo;
24import android.view.textservice.SuggestionsInfo;
25import android.view.textservice.TextInfo;
26
27import com.android.inputmethod.compat.TextInfoCompatUtils;
28import com.android.inputmethod.latin.PrevWordsInfo;
29import com.android.inputmethod.latin.utils.StringUtils;
30
31import java.util.ArrayList;
32import java.util.Locale;
33
34public 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.EMPTY_SENTENCE_SUGGESTIONS_INFOS;
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.EMPTY_SENTENCE_SUGGESTIONS_INFOS;
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