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