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