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