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 android.app.Service; 20 import android.content.Intent; 21 import android.os.Bundle; 22 import android.os.IBinder; 23 import android.os.Process; 24 import android.os.RemoteException; 25 import android.text.TextUtils; 26 import android.text.method.WordIterator; 27 import android.util.Log; 28 import android.view.textservice.SentenceSuggestionsInfo; 29 import android.view.textservice.SuggestionsInfo; 30 import android.view.textservice.TextInfo; 31 32 import com.android.internal.textservice.ISpellCheckerService; 33 import com.android.internal.textservice.ISpellCheckerServiceCallback; 34 import com.android.internal.textservice.ISpellCheckerSession; 35 import com.android.internal.textservice.ISpellCheckerSessionListener; 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 onBind(final Intent intent)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 */ createSession()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 */ setInternalISpellCheckerSession(InternalISpellCheckerSession session)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 */ onCreate()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 */ onGetSuggestions(TextInfo textInfo, int suggestionsLimit)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 */ onGetSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)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 */ onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)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 */ onCancel()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 */ onClose()219 public void onClose() {} 220 221 /** 222 * @return Locale for this session 223 */ getLocale()224 public String getLocale() { 225 return mInternalSession.getLocale(); 226 } 227 228 /** 229 * @return Bundle for this session 230 */ getBundle()231 public Bundle getBundle() { 232 return mInternalSession.getBundle(); 233 } 234 235 /** 236 * Returns result attributes supported for this session. 237 * 238 * <p>The session implementation should not set attributes that are not included in the 239 * return value of {@code getSupportedAttributes()} when creating {@link SuggestionsInfo}. 240 * 241 * @return The supported result attributes for this session 242 */ getSupportedAttributes()243 public @SuggestionsInfo.ResultAttrs int getSupportedAttributes() { 244 return mInternalSession.getSupportedAttributes(); 245 } 246 } 247 248 // Preventing from exposing ISpellCheckerSession.aidl, create an internal class. 249 private static class InternalISpellCheckerSession extends ISpellCheckerSession.Stub { 250 private ISpellCheckerSessionListener mListener; 251 private final Session mSession; 252 private final String mLocale; 253 private final Bundle mBundle; 254 private final @SuggestionsInfo.ResultAttrs int mSupportedAttributes; 255 InternalISpellCheckerSession(String locale, ISpellCheckerSessionListener listener, Bundle bundle, Session session, @SuggestionsInfo.ResultAttrs int supportedAttributes)256 public InternalISpellCheckerSession(String locale, ISpellCheckerSessionListener listener, 257 Bundle bundle, Session session, 258 @SuggestionsInfo.ResultAttrs int supportedAttributes) { 259 mListener = listener; 260 mSession = session; 261 mLocale = locale; 262 mBundle = bundle; 263 mSupportedAttributes = supportedAttributes; 264 session.setInternalISpellCheckerSession(this); 265 } 266 267 @Override onGetSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)268 public void onGetSuggestionsMultiple( 269 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 270 int pri = Process.getThreadPriority(Process.myTid()); 271 try { 272 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 273 mListener.onGetSuggestions( 274 mSession.onGetSuggestionsMultiple( 275 textInfos, suggestionsLimit, sequentialWords)); 276 } catch (RemoteException e) { 277 } finally { 278 Process.setThreadPriority(pri); 279 } 280 } 281 282 @Override onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)283 public void onGetSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { 284 try { 285 mListener.onGetSentenceSuggestions( 286 mSession.onGetSentenceSuggestionsMultiple(textInfos, suggestionsLimit)); 287 } catch (RemoteException e) { 288 } 289 } 290 291 @Override onCancel()292 public void onCancel() { 293 int pri = Process.getThreadPriority(Process.myTid()); 294 try { 295 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 296 mSession.onCancel(); 297 } finally { 298 Process.setThreadPriority(pri); 299 } 300 } 301 302 @Override onClose()303 public void onClose() { 304 int pri = Process.getThreadPriority(Process.myTid()); 305 try { 306 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 307 mSession.onClose(); 308 } finally { 309 Process.setThreadPriority(pri); 310 mListener = null; 311 } 312 } 313 getLocale()314 public String getLocale() { 315 return mLocale; 316 } 317 getBundle()318 public Bundle getBundle() { 319 return mBundle; 320 } 321 getSupportedAttributes()322 public @SuggestionsInfo.ResultAttrs int getSupportedAttributes() { 323 return mSupportedAttributes; 324 } 325 } 326 327 private static class SpellCheckerServiceBinder extends ISpellCheckerService.Stub { 328 private final WeakReference<SpellCheckerService> mInternalServiceRef; 329 SpellCheckerServiceBinder(SpellCheckerService service)330 public SpellCheckerServiceBinder(SpellCheckerService service) { 331 mInternalServiceRef = new WeakReference<SpellCheckerService>(service); 332 } 333 334 /** 335 * Called from the system when an application is requesting a new spell checker session. 336 * 337 * <p>Note: This is an internal protocol used by the system to establish spell checker 338 * sessions, which is not guaranteed to be stable and is subject to change.</p> 339 * 340 * @param locale locale to be returned from {@link Session#getLocale()} 341 * @param listener IPC channel object to be used to implement 342 * {@link Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} and 343 * {@link Session#onGetSuggestions(TextInfo, int)} 344 * @param bundle bundle to be returned from {@link Session#getBundle()} 345 * @param supportedAttributes A union of {@link SuggestionsInfo} attributes that the spell 346 * checker can set in the spell checking results. 347 * @param callback IPC channel to return the result to the caller in an asynchronous manner 348 */ 349 @Override getISpellCheckerSession( String locale, ISpellCheckerSessionListener listener, Bundle bundle, @SuggestionsInfo.ResultAttrs int supportedAttributes, ISpellCheckerServiceCallback callback)350 public void getISpellCheckerSession( 351 String locale, ISpellCheckerSessionListener listener, Bundle bundle, 352 @SuggestionsInfo.ResultAttrs int supportedAttributes, 353 ISpellCheckerServiceCallback callback) { 354 final SpellCheckerService service = mInternalServiceRef.get(); 355 final InternalISpellCheckerSession internalSession; 356 if (service == null) { 357 // If the owner SpellCheckerService object was already destroyed and got GC-ed, 358 // the weak-reference returns null and we should just ignore this request. 359 internalSession = null; 360 } else { 361 final Session session = service.createSession(); 362 internalSession = new InternalISpellCheckerSession( 363 locale, listener, bundle, session, supportedAttributes); 364 session.onCreate(); 365 } 366 try { 367 callback.onSessionCreated(internalSession); 368 } catch (RemoteException e) { 369 } 370 } 371 } 372 373 /** 374 * Adapter class to accommodate word level spell checking APIs to sentence level spell checking 375 * APIs used in 376 * {@link SpellCheckerService.Session#onGetSuggestionsMultiple(TextInfo[], int, boolean)} 377 */ 378 private static class SentenceLevelAdapter { 379 public static final SentenceSuggestionsInfo[] EMPTY_SENTENCE_SUGGESTIONS_INFOS = 380 new SentenceSuggestionsInfo[] {}; 381 private static final SuggestionsInfo EMPTY_SUGGESTIONS_INFO = new SuggestionsInfo(0, null); 382 /** 383 * Container for split TextInfo parameters 384 */ 385 public static class SentenceWordItem { 386 public final TextInfo mTextInfo; 387 public final int mStart; 388 public final int mLength; SentenceWordItem(TextInfo ti, int start, int end)389 public SentenceWordItem(TextInfo ti, int start, int end) { 390 mTextInfo = ti; 391 mStart = start; 392 mLength = end - start; 393 } 394 } 395 396 /** 397 * Container for originally queried TextInfo and parameters 398 */ 399 public static class SentenceTextInfoParams { 400 final TextInfo mOriginalTextInfo; 401 final ArrayList<SentenceWordItem> mItems; 402 final int mSize; SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items)403 public SentenceTextInfoParams(TextInfo ti, ArrayList<SentenceWordItem> items) { 404 mOriginalTextInfo = ti; 405 mItems = items; 406 mSize = items.size(); 407 } 408 } 409 410 private final WordIterator mWordIterator; SentenceLevelAdapter(Locale locale)411 public SentenceLevelAdapter(Locale locale) { 412 mWordIterator = new WordIterator(locale); 413 } 414 getSplitWords(TextInfo originalTextInfo)415 private SentenceTextInfoParams getSplitWords(TextInfo originalTextInfo) { 416 final WordIterator wordIterator = mWordIterator; 417 final CharSequence originalText = originalTextInfo.getText(); 418 final int cookie = originalTextInfo.getCookie(); 419 final int start = 0; 420 final int end = originalText.length(); 421 final ArrayList<SentenceWordItem> wordItems = new ArrayList<SentenceWordItem>(); 422 wordIterator.setCharSequence(originalText, 0, originalText.length()); 423 int wordEnd = wordIterator.following(start); 424 int wordStart = wordEnd == BreakIterator.DONE ? BreakIterator.DONE 425 : wordIterator.getBeginning(wordEnd); 426 if (DBG) { 427 Log.d(TAG, "iterator: break: ---- 1st word start = " + wordStart + ", end = " 428 + wordEnd + "\n" + originalText); 429 } 430 while (wordStart <= end && wordEnd != BreakIterator.DONE 431 && wordStart != BreakIterator.DONE) { 432 if (wordEnd >= start && wordEnd > wordStart) { 433 final CharSequence query = originalText.subSequence(wordStart, wordEnd); 434 final TextInfo ti = new TextInfo(query, 0, query.length(), cookie, 435 query.hashCode()); 436 wordItems.add(new SentenceWordItem(ti, wordStart, wordEnd)); 437 if (DBG) { 438 Log.d(TAG, "Adapter: word (" + (wordItems.size() - 1) + ") " + query); 439 } 440 } 441 wordEnd = wordIterator.following(wordEnd); 442 if (wordEnd == BreakIterator.DONE) { 443 break; 444 } 445 wordStart = wordIterator.getBeginning(wordEnd); 446 } 447 return new SentenceTextInfoParams(originalTextInfo, wordItems); 448 } 449 reconstructSuggestions( SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results)450 public static SentenceSuggestionsInfo reconstructSuggestions( 451 SentenceTextInfoParams originalTextInfoParams, SuggestionsInfo[] results) { 452 if (results == null || results.length == 0) { 453 return null; 454 } 455 if (DBG) { 456 Log.w(TAG, "Adapter: onGetSuggestions: got " + results.length); 457 } 458 if (originalTextInfoParams == null) { 459 if (DBG) { 460 Log.w(TAG, "Adapter: originalTextInfoParams is null."); 461 } 462 return null; 463 } 464 final int originalCookie = originalTextInfoParams.mOriginalTextInfo.getCookie(); 465 final int originalSequence = 466 originalTextInfoParams.mOriginalTextInfo.getSequence(); 467 468 final int querySize = originalTextInfoParams.mSize; 469 final int[] offsets = new int[querySize]; 470 final int[] lengths = new int[querySize]; 471 final SuggestionsInfo[] reconstructedSuggestions = new SuggestionsInfo[querySize]; 472 for (int i = 0; i < querySize; ++i) { 473 final SentenceWordItem item = originalTextInfoParams.mItems.get(i); 474 SuggestionsInfo result = null; 475 for (int j = 0; j < results.length; ++j) { 476 final SuggestionsInfo cur = results[j]; 477 if (cur != null && cur.getSequence() == item.mTextInfo.getSequence()) { 478 result = cur; 479 result.setCookieAndSequence(originalCookie, originalSequence); 480 break; 481 } 482 } 483 offsets[i] = item.mStart; 484 lengths[i] = item.mLength; 485 reconstructedSuggestions[i] = result != null ? result : EMPTY_SUGGESTIONS_INFO; 486 if (DBG) { 487 final int size = reconstructedSuggestions[i].getSuggestionsCount(); 488 Log.w(TAG, "reconstructedSuggestions(" + i + ")" + size + ", first = " 489 + (size > 0 ? reconstructedSuggestions[i].getSuggestionAt(0) 490 : "<none>") + ", offset = " + offsets[i] + ", length = " 491 + lengths[i]); 492 } 493 } 494 return new SentenceSuggestionsInfo(reconstructedSuggestions, offsets, lengths); 495 } 496 } 497 } 498