1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package android.service.textservice; 18 19 import com.android.internal.textservice.ISpellCheckerService; 20 import com.android.internal.textservice.ISpellCheckerSession; 21 import com.android.internal.textservice.ISpellCheckerSessionListener; 22 23 import android.app.Service; 24 import android.content.Intent; 25 import android.os.Bundle; 26 import android.os.IBinder; 27 import android.os.Process; 28 import android.os.RemoteException; 29 import android.text.TextUtils; 30 import android.text.method.WordIterator; 31 import android.util.Log; 32 import android.view.textservice.SentenceSuggestionsInfo; 33 import android.view.textservice.SuggestionsInfo; 34 import android.view.textservice.TextInfo; 35 import android.widget.SpellChecker; 36 37 import java.lang.ref.WeakReference; 38 import java.text.BreakIterator; 39 import java.util.ArrayList; 40 import java.util.Locale; 41 42 /** 43 * SpellCheckerService provides an abstract base class for a spell checker. 44 * This class combines a service to the system with the spell checker service interface that 45 * spell checker must implement. 46 * 47 * <p>In addition to the normal Service lifecycle methods, this class 48 * introduces a new specific callback that subclasses should override 49 * {@link #createSession()} to provide a spell checker session that is corresponding 50 * to requested language and so on. The spell checker session returned by this method 51 * should extend {@link SpellCheckerService.Session}. 52 * </p> 53 * 54 * <h3>Returning spell check results</h3> 55 * 56 * <p>{@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} 57 * should return spell check results. 58 * It receives {@link android.view.textservice.TextInfo} and returns 59 * {@link android.view.textservice.SuggestionsInfo} for the input. 60 * You may want to override 61 * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} for 62 * better performance and quality. 63 * </p> 64 * 65 * <p>Please note that {@link SpellCheckerService.Session#getLocale()} does not return a valid 66 * locale before {@link SpellCheckerService.Session#onCreate()} </p> 67 * 68 */ 69 public abstract class SpellCheckerService extends Service { 70 private static final String TAG = SpellCheckerService.class.getSimpleName(); 71 private static final boolean DBG = false; 72 public static final String SERVICE_INTERFACE = 73 "android.service.textservice.SpellCheckerService"; 74 75 private final SpellCheckerServiceBinder mBinder = new SpellCheckerServiceBinder(this); 76 77 78 /** 79 * Implement to return the implementation of the internal spell checker 80 * service interface. Subclasses should not override. 81 */ 82 @Override 83 public final IBinder onBind(final Intent intent) { 84 if (DBG) { 85 Log.w(TAG, "onBind"); 86 } 87 return mBinder; 88 } 89 90 /** 91 * Factory method to create a spell checker session impl 92 * @return SpellCheckerSessionImpl which should be overridden by a concrete implementation. 93 */ 94 public abstract Session createSession(); 95 96 /** 97 * This abstract class should be overridden by a concrete implementation of a spell checker. 98 */ 99 public static abstract class Session { 100 private InternalISpellCheckerSession mInternalSession; 101 private volatile SentenceLevelAdapter mSentenceLevelAdapter; 102 103 /** 104 * @hide 105 */ 106 public final void setInternalISpellCheckerSession(InternalISpellCheckerSession session) { 107 mInternalSession = session; 108 } 109 110 /** 111 * This is called after the class is initialized, at which point it knows it can call 112 * getLocale() etc... 113 */ 114 public abstract void onCreate(); 115 116 /** 117 * Get suggestions for specified text in TextInfo. 118 * This function will run on the incoming IPC thread. 119 * So, this is not called on the main thread, 120 * but will be called in series on another thread. 121 * @param textInfo the text metadata 122 * @param suggestionsLimit the maximum number of suggestions to be returned 123 * @return SuggestionsInfo which contains suggestions for textInfo 124 */ 125 public abstract SuggestionsInfo onGetSuggestions(TextInfo textInfo, int suggestionsLimit); 126 127 /** 128 * A batch process of onGetSuggestions. 129 * This function will run on the incoming IPC thread. 130 * So, this is not called on the main thread, 131 * but will be called in series on another thread. 132 * @param textInfos an array of the text metadata 133 * @param suggestionsLimit the maximum number of suggestions to be returned 134 * @param sequentialWords true if textInfos can be treated as sequential words. 135 * @return an array of {@link SentenceSuggestionsInfo} returned by 136 * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} 137 */ 138 public SuggestionsInfo[] onGetSuggestionsMultiple(TextInfo[] textInfos, 139 int suggestionsLimit, boolean sequentialWords) { 140 final int length = textInfos.length; 141 final SuggestionsInfo[] retval = new SuggestionsInfo[length]; 142 for (int i = 0; i < length; ++i) { 143 retval[i] = onGetSuggestions(textInfos[i], suggestionsLimit); 144 retval[i].setCookieAndSequence( 145 textInfos[i].getCookie(), textInfos[i].getSequence()); 146 } 147 return retval; 148 } 149 150 /** 151 * Get sentence suggestions for specified texts in an array of TextInfo. 152 * The default implementation splits the input text to words and returns 153 * {@link SentenceSuggestionsInfo} which contains suggestions for each word. 154 * This function will run on the incoming IPC thread. 155 * So, this is not called on the main thread, 156 * but will be called in series on another thread. 157 * When you override this method, make sure that suggestionsLimit is applied to suggestions 158 * that share the same start position and length. 159 * @param textInfos an array of the text metadata 160 * @param suggestionsLimit the maximum number of suggestions to be returned 161 * @return an array of {@link SentenceSuggestionsInfo} returned by 162 * {@link SpellCheckerService.Session#onGetSuggestions(TextInfo, int)} 163 */ 164 public SentenceSuggestionsInfo[] onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, 165 int suggestionsLimit) { 166 if (textInfos == null || textInfos.length == 0) { 167 return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; 168 } 169 if (DBG) { 170 Log.d(TAG, "onGetSentenceSuggestionsMultiple: + " + textInfos.length + ", " 171 + suggestionsLimit); 172 } 173 if (mSentenceLevelAdapter == null) { 174 synchronized(this) { 175 if (mSentenceLevelAdapter == null) { 176 final String localeStr = getLocale(); 177 if (!TextUtils.isEmpty(localeStr)) { 178 mSentenceLevelAdapter = new SentenceLevelAdapter(new Locale(localeStr)); 179 } 180 } 181 } 182 } 183 if (mSentenceLevelAdapter == null) { 184 return SentenceLevelAdapter.EMPTY_SENTENCE_SUGGESTIONS_INFOS; 185 } 186 final int infosSize = textInfos.length; 187 final SentenceSuggestionsInfo[] retval = new SentenceSuggestionsInfo[infosSize]; 188 for (int i = 0; i < infosSize; ++i) { 189 final SentenceLevelAdapter.SentenceTextInfoParams textInfoParams = 190 mSentenceLevelAdapter.getSplitWords(textInfos[i]); 191 final ArrayList<SentenceLevelAdapter.SentenceWordItem> mItems = 192 textInfoParams.mItems; 193 final int itemsSize = mItems.size(); 194 final TextInfo[] splitTextInfos = new TextInfo[itemsSize]; 195 for (int j = 0; j < itemsSize; ++j) { 196 splitTextInfos[j] = mItems.get(j).mTextInfo; 197 } 198 retval[i] = SentenceLevelAdapter.reconstructSuggestions( 199 textInfoParams, onGetSuggestionsMultiple( 200 splitTextInfos, suggestionsLimit, true)); 201 } 202 return retval; 203 } 204 205 /** 206 * Request to abort all tasks executed in SpellChecker. 207 * This function will run on the incoming IPC thread. 208 * So, this is not called on the main thread, 209 * but will be called in series on another thread. 210 */ 211 public void onCancel() {} 212 213 /** 214 * Request to close this session. 215 * This function will run on the incoming IPC thread. 216 * So, this is not called on the main thread, 217 * but will be called in series on another thread. 218 */ 219 public void onClose() {} 220 221 /** 222 * @return Locale for this session 223 */ 224 public String getLocale() { 225 return mInternalSession.getLocale(); 226 } 227 228 /** 229 * @return Bundle for this session 230 */ 231 public Bundle getBundle() { 232 return mInternalSession.getBundle(); 233 } 234 } 235 236 // Preventing from exposing ISpellCheckerSession.aidl, create an internal class. 237 private static class InternalISpellCheckerSession extends ISpellCheckerSession.Stub { 238 private ISpellCheckerSessionListener mListener; 239 private final Session mSession; 240 private final String mLocale; 241 private final Bundle mBundle; 242 243 public InternalISpellCheckerSession(String locale, ISpellCheckerSessionListener listener, 244 Bundle bundle, Session session) { 245 mListener = listener; 246 mSession = session; 247 mLocale = locale; 248 mBundle = bundle; 249 session.setInternalISpellCheckerSession(this); 250 } 251 252 @Override 253 public void onGetSuggestionsMultiple( 254 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 255 int pri = Process.getThreadPriority(Process.myTid()); 256 try { 257 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 258 mListener.onGetSuggestions( 259 mSession.onGetSuggestionsMultiple( 260 textInfos, suggestionsLimit, sequentialWords)); 261 } catch (RemoteException e) { 262 } finally { 263 Process.setThreadPriority(pri); 264 } 265 } 266 267 @Override 268 public void onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { 269 try { 270 mListener.onGetSentenceSuggestions( 271 mSession.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit)); 272 } catch (RemoteException e) { 273 } 274 } 275 276 @Override 277 public void onCancel() { 278 int pri = Process.getThreadPriority(Process.myTid()); 279 try { 280 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 281 mSession.onCancel(); 282 } finally { 283 Process.setThreadPriority(pri); 284 } 285 } 286 287 @Override 288 public void onClose() { 289 int pri = Process.getThreadPriority(Process.myTid()); 290 try { 291 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 292 mSession.onClose(); 293 } finally { 294 Process.setThreadPriority(pri); 295 mListener = null; 296 } 297 } 298 299 public String getLocale() { 300 return mLocale; 301 } 302 303 public Bundle getBundle() { 304 return mBundle; 305 } 306 } 307 308 private static class SpellCheckerServiceBinder extends ISpellCheckerService.Stub { 309 private final WeakReference<SpellCheckerService> mInternalServiceRef; 310 311 public SpellCheckerServiceBinder(SpellCheckerService service) { 312 mInternalServiceRef = new WeakReference<SpellCheckerService>(service); 313 } 314 315 @Override 316 public ISpellCheckerSession getISpellCheckerSession( 317 String locale, ISpellCheckerSessionListener listener, Bundle bundle) { 318 final SpellCheckerService service = mInternalServiceRef.get(); 319 if (service == null) return null; 320 final Session session = service.createSession(); 321 final InternalISpellCheckerSession internalSession = 322 new InternalISpellCheckerSession(locale, listener, bundle, session); 323 session.onCreate(); 324 return internalSession; 325 } 326 } 327 328 /** 329 * Adapter class to accommodate word level spell checking APIs to sentence level spell checking 330 * APIs used in 331 * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} 332 */ 333 private static class SentenceLevelAdapter { 334 public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = 335 new SentenceSuggestionsInfo[] {}; 336 private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); 337 /** 338 * Container for split TextInfo parameters 339 */ 340 public static class SentenceWordItem { 341 public final TextInfo mTextInfo; 342 public final int mStart; 343 public final int mLength; 344 public SentenceWordItem(TextInfo ti, int start, int end) { 345 mTextInfo = ti; 346 mStart = start; 347 mLength = end - start; 348 } 349 } 350 351 /** 352 * Container for originally queried TextInfo and parameters 353 */ 354 public static class SentenceTextInfoParams { 355 final TextInfo mOriginalTextInfo; 356 final ArrayList<SentenceWordItem> mItems; 357 final int mSize; 358 public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { 359 mOriginalTextInfo = ti; 360 mItems = items; 361 mSize = items.size(); 362 } 363 } 364 365 private final WordIterator mWordIterator; 366 public SentenceLevelAdapter(Locale locale) { 367 mWordIterator = new WordIterator(locale); 368 } 369 370 private SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { 371 final WordIterator wordIterator = mWordIterator; 372 final CharSequence originalText = originalTextInfo.getText(); 373 final int cookie = originalTextInfo.getCookie(); 374 final int start = 0; 375 final int end = originalText.length(); 376 final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>(); 377 wordIterator.setCharSequence(originalText, 0, originalText.length()); 378 int wordEnd = wordIterator.following(start); 379 int wordStart = wordIterator.getBeginning(wordEnd); 380 if (DBG) { 381 Log.d(TAG, "iterator: break: ---- 1st word start = " + wordStart + ", end = " 382 + wordEnd + "\n" + originalText); 383 } 384 while (wordStart <= end && wordEnd != BreakIterator.DONE 385 && wordStart != BreakIterator.DONE) { 386 if (wordEnd >= start && wordEnd > wordStart) { 387 final String query = originalText.subSequence(wordStart, wordEnd).toString(); 388 final TextInfo ti = new TextInfo(query, cookie, query.hashCode()); 389 wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); 390 if (DBG) { 391 Log.d(TAG, "Adapter: word (" + (wordItems.size() - 1) + ") " + query); 392 } 393 } 394 wordEnd = wordIterator.following(wordEnd); 395 if (wordEnd == BreakIterator.DONE) { 396 break; 397 } 398 wordStart = wordIterator.getBeginning(wordEnd); 399 } 400 return new SentenceTextInfoParams(originalTextInfo, wordItems); 401 } 402 403 public static SentenceSuggestionsInfo reconstructSuggestions( 404 SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { 405 if (results == null || results.length == 0) { 406 return null; 407 } 408 if (DBG) { 409 Log.w(TAG, "Adapter: onGetSuggestions: got " + results.length); 410 } 411 if (originalTextInfoParams == null) { 412 if (DBG) { 413 Log.w(TAG, "Adapter: originalTextInfoParams is null."); 414 } 415 return null; 416 } 417 final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); 418 final int originalSequence = 419 originalTextInfoParams.mOriginalTextInfo.getSequence(); 420 421 final int querySize = originalTextInfoParams.mSize; 422 final int[] offsets = new int[querySize]; 423 final int[] lengths = new int[querySize]; 424 final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; 425 for (int i = 0; i < querySize; ++i) { 426 final SentenceWordItem item = originalTextInfoParams.mItems.get(i); 427 SuggestionsInfo result = null; 428 for (int j = 0; j < results.length; ++j) { 429 final SuggestionsInfo cur = results[j]; 430 if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { 431 result = cur; 432 result.setCookieAndSequence(originalCookie, originalSequence); 433 break; 434 } 435 } 436 offsets[i] = item.mStart; 437 lengths[i] = item.mLength; 438 reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; 439 if (DBG) { 440 final int size = reconstructedSuggestions[i].getSuggestionsCount(); 441 Log.w(TAG, "reconstructedSuggestions(" + i + ")" + size + ", first = " 442 + (size > 0 ? reconstructedSuggestions[i].getSuggestionAt(0) 443 : "<none>") + ", offset = " + offsets[i] + ", length = " 444 + lengths[i]); 445 } 446 } 447 return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); 448 } 449 } 450 } 451