1 /* 2 * Copyright (C) 2011 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 android.view.textservice; 18 19 import android.annotation.BinderThread; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SuppressLint; 23 import android.compat.annotation.UnsupportedAppUsage; 24 import android.os.Binder; 25 import android.os.Build; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.HandlerThread; 29 import android.os.Message; 30 import android.os.Process; 31 import android.os.RemoteException; 32 import android.util.Log; 33 import android.view.inputmethod.InputMethodManager; 34 35 import com.android.internal.annotations.GuardedBy; 36 import com.android.internal.textservice.ISpellCheckerSession; 37 import com.android.internal.textservice.ISpellCheckerSessionListener; 38 import com.android.internal.textservice.ITextServicesSessionListener; 39 40 import dalvik.system.CloseGuard; 41 42 import java.util.ArrayDeque; 43 import java.util.Locale; 44 import java.util.Queue; 45 import java.util.concurrent.Executor; 46 47 /** 48 * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService. 49 * 50 * 51 * <a name="Applications"></a> 52 * <h3>Applications</h3> 53 * 54 * <p>In most cases, applications that are using the standard 55 * {@link android.widget.TextView} or its subclasses will have little they need 56 * to do to work well with spell checker services. The main things you need to 57 * be aware of are:</p> 58 * 59 * <ul> 60 * <li> Properly set the {@link android.R.attr#inputType} in your editable 61 * text views, so that the spell checker will have enough context to help the 62 * user in editing text in them. 63 * </ul> 64 * 65 * <p>For the rare people amongst us writing client applications that use the spell checker service 66 * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or 67 * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker 68 * service by yourself.</p> 69 * 70 * <h3>Security</h3> 71 * 72 * <p>There are a lot of security issues associated with spell checkers, 73 * since they could monitor all the text being sent to them 74 * through, for instance, {@link android.widget.TextView}. 75 * The Android spell checker framework also allows 76 * arbitrary third party spell checkers, so care must be taken to restrict their 77 * selection and interactions.</p> 78 * 79 * <p>Here are some key points about the security architecture behind the 80 * spell checker framework:</p> 81 * 82 * <ul> 83 * <li>Only the system is allowed to directly access a spell checker framework's 84 * {@link android.service.textservice.SpellCheckerService} interface, via the 85 * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission. This is 86 * enforced in the system by not binding to a spell checker service that does 87 * not require this permission. 88 * 89 * <li>The user must explicitly enable a new spell checker in settings before 90 * they can be enabled, to confirm with the system that they know about it 91 * and want to make it available for use. 92 * </ul> 93 * 94 */ 95 public class SpellCheckerSession { 96 private static final String TAG = SpellCheckerSession.class.getSimpleName(); 97 private static final boolean DBG = false; 98 /** 99 * Name under which a SpellChecker service component publishes information about itself. 100 * This meta-data must reference an XML resource. 101 **/ 102 public static final String SERVICE_META_DATA = "android.view.textservice.scs"; 103 104 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; 105 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2; 106 107 private final InternalListener mInternalListener; 108 private final TextServicesManager mTextServicesManager; 109 private final SpellCheckerInfo mSpellCheckerInfo; 110 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 111 private final SpellCheckerSessionListener mSpellCheckerSessionListener; 112 private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; 113 private final Executor mExecutor; 114 115 private final CloseGuard mGuard = CloseGuard.get(); 116 117 /** 118 * Constructor 119 * @hide 120 */ SpellCheckerSession( SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener, Executor executor)121 public SpellCheckerSession( 122 SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener, 123 Executor executor) { 124 if (info == null || listener == null || tsm == null) { 125 throw new NullPointerException(); 126 } 127 mSpellCheckerInfo = info; 128 mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(this); 129 mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl); 130 mTextServicesManager = tsm; 131 mSpellCheckerSessionListener = listener; 132 mExecutor = executor; 133 134 mGuard.open("finishSession"); 135 } 136 137 /** 138 * @return true if the connection to a text service of this session is disconnected and not 139 * alive. 140 */ isSessionDisconnected()141 public boolean isSessionDisconnected() { 142 return mSpellCheckerSessionListenerImpl.isDisconnected(); 143 } 144 145 /** 146 * Get the spell checker service info this spell checker session has. 147 * @return SpellCheckerInfo for the specified locale. 148 */ getSpellChecker()149 public SpellCheckerInfo getSpellChecker() { 150 return mSpellCheckerInfo; 151 } 152 153 /** 154 * Cancel pending and running spell check tasks 155 */ cancel()156 public void cancel() { 157 mSpellCheckerSessionListenerImpl.cancel(); 158 } 159 160 /** 161 * Finish this session and allow TextServicesManagerService to disconnect the bound spell 162 * checker. 163 */ close()164 public void close() { 165 mGuard.close(); 166 mSpellCheckerSessionListenerImpl.close(); 167 mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl); 168 } 169 170 /** 171 * Get suggestions from the specified sentences 172 * @param textInfos an array of text metadata for a spell checker 173 * @param suggestionsLimit the maximum number of suggestions that will be returned 174 */ getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit)175 public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) { 176 final InputMethodManager imm = mTextServicesManager.getInputMethodManager(); 177 if (imm != null && imm.isInputMethodSuppressingSpellChecker()) { 178 handleOnGetSentenceSuggestionsMultiple(new SentenceSuggestionsInfo[0]); 179 return; 180 } 181 mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple( 182 textInfos, suggestionsLimit); 183 } 184 185 /** 186 * Get candidate strings for a substring of the specified text. 187 * @param textInfo text metadata for a spell checker 188 * @param suggestionsLimit the maximum number of suggestions that will be returned 189 * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead 190 */ 191 @Deprecated getSuggestions(TextInfo textInfo, int suggestionsLimit)192 public void getSuggestions(TextInfo textInfo, int suggestionsLimit) { 193 getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false); 194 } 195 196 /** 197 * A batch process of getSuggestions 198 * @param textInfos an array of text metadata for a spell checker 199 * @param suggestionsLimit the maximum number of suggestions that will be returned 200 * @param sequentialWords true if textInfos can be treated as sequential words. 201 * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead 202 */ 203 @Deprecated getSuggestions( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)204 public void getSuggestions( 205 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 206 if (DBG) { 207 Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId()); 208 } 209 final InputMethodManager imm = mTextServicesManager.getInputMethodManager(); 210 if (imm != null && imm.isInputMethodSuppressingSpellChecker()) { 211 handleOnGetSuggestionsMultiple(new SuggestionsInfo[0]); 212 return; 213 } 214 mSpellCheckerSessionListenerImpl.getSuggestionsMultiple( 215 textInfos, suggestionsLimit, sequentialWords); 216 } 217 handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionsInfos)218 void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionsInfos) { 219 mExecutor.execute(() -> mSpellCheckerSessionListener.onGetSuggestions(suggestionsInfos)); 220 } 221 handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionsInfos)222 void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionsInfos) { 223 mExecutor.execute(() -> 224 mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionsInfos)); 225 } 226 227 private static final class SpellCheckerSessionListenerImpl 228 extends ISpellCheckerSessionListener.Stub { 229 private static final int TASK_CANCEL = 1; 230 private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; 231 private static final int TASK_CLOSE = 3; 232 private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4; taskToString(int task)233 private static String taskToString(int task) { 234 switch (task) { 235 case TASK_CANCEL: 236 return "TASK_CANCEL"; 237 case TASK_GET_SUGGESTIONS_MULTIPLE: 238 return "TASK_GET_SUGGESTIONS_MULTIPLE"; 239 case TASK_CLOSE: 240 return "TASK_CLOSE"; 241 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: 242 return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE"; 243 default: 244 return "Unexpected task=" + task; 245 } 246 } 247 248 private final Queue<SpellCheckerParams> mPendingTasks = new ArrayDeque<>(); 249 @GuardedBy("SpellCheckerSessionListenerImpl.this") 250 private SpellCheckerSession mSpellCheckerSession; 251 252 private static final int STATE_WAIT_CONNECTION = 0; 253 private static final int STATE_CONNECTED = 1; 254 private static final int STATE_CLOSED_AFTER_CONNECTION = 2; 255 private static final int STATE_CLOSED_BEFORE_CONNECTION = 3; stateToString(int state)256 private static String stateToString(int state) { 257 switch (state) { 258 case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION"; 259 case STATE_CONNECTED: return "STATE_CONNECTED"; 260 case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION"; 261 case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION"; 262 default: return "Unexpected state=" + state; 263 } 264 } 265 private int mState = STATE_WAIT_CONNECTION; 266 267 private ISpellCheckerSession mISpellCheckerSession; 268 private HandlerThread mThread; 269 private Handler mAsyncHandler; 270 SpellCheckerSessionListenerImpl(SpellCheckerSession spellCheckerSession)271 SpellCheckerSessionListenerImpl(SpellCheckerSession spellCheckerSession) { 272 mSpellCheckerSession = spellCheckerSession; 273 } 274 275 private static class SpellCheckerParams { 276 public final int mWhat; 277 public final TextInfo[] mTextInfos; 278 public final int mSuggestionsLimit; 279 public final boolean mSequentialWords; 280 public ISpellCheckerSession mSession; SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)281 public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, 282 boolean sequentialWords) { 283 mWhat = what; 284 mTextInfos = textInfos; 285 mSuggestionsLimit = suggestionsLimit; 286 mSequentialWords = sequentialWords; 287 } 288 } 289 processTask(ISpellCheckerSession session, SpellCheckerParams scp, boolean async)290 private void processTask(ISpellCheckerSession session, SpellCheckerParams scp, 291 boolean async) { 292 if (DBG) { 293 synchronized (this) { 294 Log.d(TAG, "entering processTask:" 295 + " session.hashCode()=#" + Integer.toHexString(session.hashCode()) 296 + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async 297 + " mAsyncHandler=" + mAsyncHandler 298 + " mState=" + stateToString(mState)); 299 } 300 } 301 if (async || mAsyncHandler == null) { 302 switch (scp.mWhat) { 303 case TASK_CANCEL: 304 try { 305 session.onCancel(); 306 } catch (RemoteException e) { 307 Log.e(TAG, "Failed to cancel " + e); 308 } 309 break; 310 case TASK_GET_SUGGESTIONS_MULTIPLE: 311 try { 312 session.onGetSuggestionsMultiple(scp.mTextInfos, 313 scp.mSuggestionsLimit, scp.mSequentialWords); 314 } catch (RemoteException e) { 315 Log.e(TAG, "Failed to get suggestions " + e); 316 } 317 break; 318 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: 319 try { 320 session.onGetSentenceSuggestionsMultiple( 321 scp.mTextInfos, scp.mSuggestionsLimit); 322 } catch (RemoteException e) { 323 Log.e(TAG, "Failed to get suggestions " + e); 324 } 325 break; 326 case TASK_CLOSE: 327 try { 328 session.onClose(); 329 } catch (RemoteException e) { 330 Log.e(TAG, "Failed to close " + e); 331 } 332 break; 333 } 334 } else { 335 // The interface is to a local object, so need to execute it 336 // asynchronously. 337 scp.mSession = session; 338 mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp)); 339 } 340 341 if (scp.mWhat == TASK_CLOSE) { 342 // If we are closing, we want to clean up our state now even 343 // if it is pending as an async operation. 344 synchronized (this) { 345 processCloseLocked(); 346 } 347 } 348 } 349 350 @GuardedBy("SpellCheckerSessionListenerImpl.this") processCloseLocked()351 private void processCloseLocked() { 352 if (DBG) Log.d(TAG, "entering processCloseLocked:" 353 + " session" + (mISpellCheckerSession != null ? ".hashCode()=#" 354 + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null") 355 + " mState=" + stateToString(mState)); 356 mISpellCheckerSession = null; 357 if (mThread != null) { 358 mThread.quit(); 359 } 360 mSpellCheckerSession = null; 361 mPendingTasks.clear(); 362 mThread = null; 363 mAsyncHandler = null; 364 switch (mState) { 365 case STATE_WAIT_CONNECTION: 366 mState = STATE_CLOSED_BEFORE_CONNECTION; 367 break; 368 case STATE_CONNECTED: 369 mState = STATE_CLOSED_AFTER_CONNECTION; 370 break; 371 default: 372 Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" + 373 stateToString(mState)); 374 break; 375 } 376 } 377 onServiceConnected(ISpellCheckerSession session)378 public void onServiceConnected(ISpellCheckerSession session) { 379 synchronized (this) { 380 switch (mState) { 381 case STATE_WAIT_CONNECTION: 382 // OK, go ahead. 383 break; 384 case STATE_CLOSED_BEFORE_CONNECTION: 385 // This is possible, and not an error. The client no longer is interested 386 // in this connection. OK to ignore. 387 if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is" 388 + " already closed."); 389 return; 390 default: 391 Log.e(TAG, "ignoring onServiceConnected due to unexpected mState=" 392 + stateToString(mState)); 393 return; 394 } 395 if (session == null) { 396 Log.e(TAG, "ignoring onServiceConnected due to session=null"); 397 return; 398 } 399 mISpellCheckerSession = session; 400 if (session.asBinder() instanceof Binder && mThread == null) { 401 if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected."); 402 // If this is a local object, we need to do our own threading 403 // to make sure we handle it asynchronously. 404 mThread = new HandlerThread("SpellCheckerSession", 405 Process.THREAD_PRIORITY_BACKGROUND); 406 mThread.start(); 407 mAsyncHandler = new Handler(mThread.getLooper()) { 408 @Override public void handleMessage(Message msg) { 409 SpellCheckerParams scp = (SpellCheckerParams)msg.obj; 410 processTask(scp.mSession, scp, true); 411 } 412 }; 413 } 414 mState = STATE_CONNECTED; 415 if (DBG) { 416 Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#" 417 + Integer.toHexString(mISpellCheckerSession.hashCode()) 418 + " mPendingTasks.size()=" + mPendingTasks.size()); 419 } 420 while (!mPendingTasks.isEmpty()) { 421 processTask(session, mPendingTasks.poll(), false); 422 } 423 } 424 } 425 cancel()426 public void cancel() { 427 processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false)); 428 } 429 getSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)430 public void getSuggestionsMultiple( 431 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 432 processOrEnqueueTask( 433 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos, 434 suggestionsLimit, sequentialWords)); 435 } 436 getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)437 public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { 438 processOrEnqueueTask( 439 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE, 440 textInfos, suggestionsLimit, false)); 441 } 442 close()443 public void close() { 444 processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false)); 445 } 446 isDisconnected()447 public boolean isDisconnected() { 448 synchronized (this) { 449 return mState != STATE_CONNECTED; 450 } 451 } 452 processOrEnqueueTask(SpellCheckerParams scp)453 private void processOrEnqueueTask(SpellCheckerParams scp) { 454 ISpellCheckerSession session; 455 synchronized (this) { 456 if (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION 457 || mState == STATE_CLOSED_BEFORE_CONNECTION)) { 458 // It is OK to call SpellCheckerSession#close() multiple times. 459 // Don't output confusing/misleading warning messages. 460 return; 461 } 462 if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) { 463 Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState=" 464 + stateToString(mState) 465 + " scp.mWhat=" + taskToString(scp.mWhat)); 466 return; 467 } 468 469 if (mState == STATE_WAIT_CONNECTION) { 470 // If we are still waiting for the connection. Need to pay special attention. 471 if (scp.mWhat == TASK_CLOSE) { 472 processCloseLocked(); 473 return; 474 } 475 // Enqueue the task to task queue. 476 SpellCheckerParams closeTask = null; 477 if (scp.mWhat == TASK_CANCEL) { 478 if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask."); 479 while (!mPendingTasks.isEmpty()) { 480 final SpellCheckerParams tmp = mPendingTasks.poll(); 481 if (tmp.mWhat == TASK_CLOSE) { 482 // Only one close task should be processed, while we need to remove 483 // all close tasks from the queue 484 closeTask = tmp; 485 } 486 } 487 } 488 mPendingTasks.offer(scp); 489 if (closeTask != null) { 490 mPendingTasks.offer(closeTask); 491 } 492 if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the" 493 + " connection is not established." 494 + " mPendingTasks.size()=" + mPendingTasks.size()); 495 return; 496 } 497 498 session = mISpellCheckerSession; 499 } 500 // session must never be null here. 501 processTask(session, scp, false); 502 } 503 504 @BinderThread 505 @Override onGetSuggestions(SuggestionsInfo[] results)506 public void onGetSuggestions(SuggestionsInfo[] results) { 507 SpellCheckerSession session = getSpellCheckerSession(); 508 if (session != null) { 509 // Lock should not be held when calling callback, in order to avoid deadlock. 510 session.handleOnGetSuggestionsMultiple(results); 511 } 512 } 513 514 @BinderThread 515 @Override onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)516 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { 517 SpellCheckerSession session = getSpellCheckerSession(); 518 if (session != null) { 519 // Lock should not be held when calling callback, in order to avoid deadlock. 520 session.handleOnGetSentenceSuggestionsMultiple(results); 521 } 522 } 523 524 @Nullable getSpellCheckerSession()525 private SpellCheckerSession getSpellCheckerSession() { 526 synchronized (SpellCheckerSessionListenerImpl.this) { 527 return mSpellCheckerSession; 528 } 529 } 530 } 531 532 /** Parameters used to create a {@link SpellCheckerSession}. */ 533 public static class SpellCheckerSessionParams { 534 @Nullable 535 private final Locale mLocale; 536 private final boolean mShouldReferToSpellCheckerLanguageSettings; 537 private final @SuggestionsInfo.ResultAttrs int mSupportedAttributes; 538 private final Bundle mExtras; 539 SpellCheckerSessionParams(Locale locale, boolean referToSpellCheckerLanguageSettings, int supportedAttributes, Bundle extras)540 private SpellCheckerSessionParams(Locale locale, 541 boolean referToSpellCheckerLanguageSettings, int supportedAttributes, 542 Bundle extras) { 543 mLocale = locale; 544 mShouldReferToSpellCheckerLanguageSettings = referToSpellCheckerLanguageSettings; 545 mSupportedAttributes = supportedAttributes; 546 mExtras = extras; 547 } 548 549 /** 550 * Returns the locale in which the spell checker should operate. 551 * 552 * @see android.service.textservice.SpellCheckerService.Session#getLocale() 553 */ 554 @SuppressLint("UseIcu") 555 @Nullable getLocale()556 public Locale getLocale() { 557 return mLocale; 558 } 559 560 /** 561 * Returns true if the user's spell checker language settings should be used to determine 562 * the spell checker locale. 563 */ shouldReferToSpellCheckerLanguageSettings()564 public boolean shouldReferToSpellCheckerLanguageSettings() { 565 return mShouldReferToSpellCheckerLanguageSettings; 566 } 567 568 /** 569 * Returns a bitmask of {@link SuggestionsInfo} attributes that the spell checker can set 570 * in {@link SuggestionsInfo} it returns. 571 * 572 * @see android.service.textservice.SpellCheckerService.Session#getSupportedAttributes() 573 */ getSupportedAttributes()574 public @SuggestionsInfo.ResultAttrs int getSupportedAttributes() { 575 return mSupportedAttributes; 576 } 577 578 /** 579 * Returns a bundle containing extra parameters for the spell checker. 580 * 581 * <p>This bundle can be used to pass implementation-specific parameters to the 582 * {@link android.service.textservice.SpellCheckerService} implementation. 583 * 584 * @see android.service.textservice.SpellCheckerService.Session#getBundle() 585 */ 586 @NonNull getExtras()587 public Bundle getExtras() { 588 return mExtras; 589 } 590 591 /** Builder of {@link SpellCheckerSessionParams}. */ 592 public static final class Builder { 593 @Nullable 594 private Locale mLocale; 595 private boolean mShouldReferToSpellCheckerLanguageSettings = false; 596 private @SuggestionsInfo.ResultAttrs int mSupportedAttributes = 0; 597 private Bundle mExtras = Bundle.EMPTY; 598 599 /** Constructs a {@code Builder}. */ Builder()600 public Builder() { 601 } 602 603 /** 604 * Returns constructed {@link SpellCheckerSession} instance. 605 * 606 * <p>Before calling this method, either {@link #setLocale(Locale)} should be called 607 * with a non-null locale or 608 * {@link #setShouldReferToSpellCheckerLanguageSettings(boolean)} should be called with 609 * {@code true}. 610 */ 611 @NonNull build()612 public SpellCheckerSessionParams build() { 613 if (mLocale == null && !mShouldReferToSpellCheckerLanguageSettings) { 614 throw new IllegalArgumentException("mLocale should not be null if " 615 + " mShouldReferToSpellCheckerLanguageSettings is false."); 616 } 617 return new SpellCheckerSessionParams(mLocale, 618 mShouldReferToSpellCheckerLanguageSettings, mSupportedAttributes, mExtras); 619 } 620 621 /** 622 * Sets the locale in which the spell checker should operate. 623 * 624 * @see android.service.textservice.SpellCheckerService.Session#getLocale() 625 */ 626 @NonNull setLocale(@uppressLint"UseIcu") @ullable Locale locale)627 public Builder setLocale(@SuppressLint("UseIcu") @Nullable Locale locale) { 628 mLocale = locale; 629 return this; 630 } 631 632 /** 633 * Sets whether or not the user's spell checker language settings should be used to 634 * determine spell checker locale. 635 * 636 * <p>If {@code shouldReferToSpellCheckerLanguageSettings} is true, the exact way of 637 * determining spell checker locale differs based on {@code locale} specified in 638 * {@link #setLocale(Locale)}. 639 * If {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is 640 * null, the locale specified in Settings will be used. If 641 * {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is not 642 * null, {@link SpellCheckerSession} can be created only when the locale specified in 643 * Settings is the same as {@code locale}. Exceptionally, if 644 * {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is 645 * language only (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be 646 * used. 647 * 648 * @see #setLocale(Locale) 649 */ 650 @NonNull setShouldReferToSpellCheckerLanguageSettings( boolean shouldReferToSpellCheckerLanguageSettings)651 public Builder setShouldReferToSpellCheckerLanguageSettings( 652 boolean shouldReferToSpellCheckerLanguageSettings) { 653 mShouldReferToSpellCheckerLanguageSettings = 654 shouldReferToSpellCheckerLanguageSettings; 655 return this; 656 } 657 658 /** 659 * Sets a bitmask of {@link SuggestionsInfo} attributes that the spell checker can set 660 * in {@link SuggestionsInfo} it returns. 661 * 662 * @see android.service.textservice.SpellCheckerService.Session#getSupportedAttributes() 663 */ 664 @NonNull setSupportedAttributes( @uggestionsInfo.ResultAttrs int supportedAttributes)665 public Builder setSupportedAttributes( 666 @SuggestionsInfo.ResultAttrs int supportedAttributes) { 667 mSupportedAttributes = supportedAttributes; 668 return this; 669 } 670 671 /** 672 * Sets a bundle containing extra parameters for the spell checker. 673 * 674 * <p>This bundle can be used to pass implementation-specific parameters to the 675 * {@link android.service.textservice.SpellCheckerService} implementation. 676 * 677 * @see android.service.textservice.SpellCheckerService.Session#getBundle() 678 */ 679 @NonNull setExtras(@onNull Bundle extras)680 public Builder setExtras(@NonNull Bundle extras) { 681 mExtras = extras; 682 return this; 683 } 684 } 685 } 686 687 /** 688 * Callback for getting results from text services 689 */ 690 public interface SpellCheckerSessionListener { 691 /** 692 * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)} 693 * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 694 * @param results an array of {@link SuggestionsInfo}s. 695 * These results are suggestions for {@link TextInfo}s queried by 696 * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or 697 * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 698 */ onGetSuggestions(SuggestionsInfo[] results)699 public void onGetSuggestions(SuggestionsInfo[] results); 700 /** 701 * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} 702 * @param results an array of {@link SentenceSuggestionsInfo}s. 703 * These results are suggestions for {@link TextInfo}s 704 * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}. 705 */ onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)706 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results); 707 } 708 709 private static final class InternalListener extends ITextServicesSessionListener.Stub { 710 private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl; 711 InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl)712 public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) { 713 mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl; 714 } 715 716 @Override onServiceConnected(ISpellCheckerSession session)717 public void onServiceConnected(ISpellCheckerSession session) { 718 mParentSpellCheckerSessionListenerImpl.onServiceConnected(session); 719 } 720 } 721 722 @Override finalize()723 protected void finalize() throws Throwable { 724 try { 725 // Note that mGuard will be null if the constructor threw. 726 if (mGuard != null) { 727 mGuard.warnIfOpen(); 728 close(); 729 } 730 } finally { 731 super.finalize(); 732 } 733 } 734 735 /** 736 * @hide 737 */ getTextServicesSessionListener()738 public ITextServicesSessionListener getTextServicesSessionListener() { 739 return mInternalListener; 740 } 741 742 /** 743 * @hide 744 */ getSpellCheckerSessionListener()745 public ISpellCheckerSessionListener getSpellCheckerSessionListener() { 746 return mSpellCheckerSessionListenerImpl; 747 } 748 } 749