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 
AndroidSpellCheckerSession(AndroidSpellCheckerService service)42     public AndroidSpellCheckerSession(AndroidSpellCheckerService service) {
43         super(service);
44         mResources = service.getResources();
45     }
46 
47     @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
fixWronglyInvalidatedWordWithSingleQuote(TextInfo ti, SentenceSuggestionsInfo ssi)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
onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)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      */
splitAndSuggest(TextInfo[] textInfos, int suggestionsLimit)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
onGetSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)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