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 40 public AndroidSpellCheckerSession(AndroidSpellCheckerService service) { 41 super(service); 42 mResources = service.getResources(); 43 } 44 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 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 */ 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 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