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 42 public AndroidSpellCheckerSession(AndroidSpellCheckerService service) { 43 super(service); 44 mResources = service.getResources(); 45 } 46 47 @TargetApi(Build.VERSION_CODES.JELLY_BEAN) 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 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 */ 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 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