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.compat.annotation.UnsupportedAppUsage; 20 import android.os.Binder; 21 import android.os.Handler; 22 import android.os.HandlerThread; 23 import android.os.Message; 24 import android.os.Process; 25 import android.os.RemoteException; 26 import android.util.Log; 27 28 import com.android.internal.textservice.ISpellCheckerSession; 29 import com.android.internal.textservice.ISpellCheckerSessionListener; 30 import com.android.internal.textservice.ITextServicesSessionListener; 31 32 import dalvik.system.CloseGuard; 33 34 import java.util.LinkedList; 35 import java.util.Queue; 36 37 /** 38 * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService. 39 * 40 * 41 * <a name="Applications"></a> 42 * <h3>Applications</h3> 43 * 44 * <p>In most cases, applications that are using the standard 45 * {@link android.widget.TextView} or its subclasses will have little they need 46 * to do to work well with spell checker services. The main things you need to 47 * be aware of are:</p> 48 * 49 * <ul> 50 * <li> Properly set the {@link android.R.attr#inputType} in your editable 51 * text views, so that the spell checker will have enough context to help the 52 * user in editing text in them. 53 * </ul> 54 * 55 * <p>For the rare people amongst us writing client applications that use the spell checker service 56 * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or 57 * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker 58 * service by yourself.</p> 59 * 60 * <h3>Security</h3> 61 * 62 * <p>There are a lot of security issues associated with spell checkers, 63 * since they could monitor all the text being sent to them 64 * through, for instance, {@link android.widget.TextView}. 65 * The Android spell checker framework also allows 66 * arbitrary third party spell checkers, so care must be taken to restrict their 67 * selection and interactions.</p> 68 * 69 * <p>Here are some key points about the security architecture behind the 70 * spell checker framework:</p> 71 * 72 * <ul> 73 * <li>Only the system is allowed to directly access a spell checker framework's 74 * {@link android.service.textservice.SpellCheckerService} interface, via the 75 * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission. This is 76 * enforced in the system by not binding to a spell checker service that does 77 * not require this permission. 78 * 79 * <li>The user must explicitly enable a new spell checker in settings before 80 * they can be enabled, to confirm with the system that they know about it 81 * and want to make it available for use. 82 * </ul> 83 * 84 */ 85 public class SpellCheckerSession { 86 private static final String TAG = SpellCheckerSession.class.getSimpleName(); 87 private static final boolean DBG = false; 88 /** 89 * Name under which a SpellChecker service component publishes information about itself. 90 * This meta-data must reference an XML resource. 91 **/ 92 public static final String SERVICE_META_DATA = "android.view.textservice.scs"; 93 94 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; 95 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2; 96 97 private final InternalListener mInternalListener; 98 private final TextServicesManager mTextServicesManager; 99 private final SpellCheckerInfo mSpellCheckerInfo; 100 @UnsupportedAppUsage 101 private final SpellCheckerSessionListener mSpellCheckerSessionListener; 102 private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; 103 104 private final CloseGuard mGuard = CloseGuard.get(); 105 106 /** Handler that will execute the main tasks */ 107 private final Handler mHandler = new Handler() { 108 @Override 109 public void handleMessage(Message msg) { 110 switch (msg.what) { 111 case MSG_ON_GET_SUGGESTION_MULTIPLE: 112 handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); 113 break; 114 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE: 115 handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj); 116 break; 117 } 118 } 119 }; 120 121 /** 122 * Constructor 123 * @hide 124 */ SpellCheckerSession( SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener)125 public SpellCheckerSession( 126 SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener) { 127 if (info == null || listener == null || tsm == null) { 128 throw new NullPointerException(); 129 } 130 mSpellCheckerInfo = info; 131 mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler); 132 mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl); 133 mTextServicesManager = tsm; 134 mSpellCheckerSessionListener = listener; 135 136 mGuard.open("finishSession"); 137 } 138 139 /** 140 * @return true if the connection to a text service of this session is disconnected and not 141 * alive. 142 */ isSessionDisconnected()143 public boolean isSessionDisconnected() { 144 return mSpellCheckerSessionListenerImpl.isDisconnected(); 145 } 146 147 /** 148 * Get the spell checker service info this spell checker session has. 149 * @return SpellCheckerInfo for the specified locale. 150 */ getSpellChecker()151 public SpellCheckerInfo getSpellChecker() { 152 return mSpellCheckerInfo; 153 } 154 155 /** 156 * Cancel pending and running spell check tasks 157 */ cancel()158 public void cancel() { 159 mSpellCheckerSessionListenerImpl.cancel(); 160 } 161 162 /** 163 * Finish this session and allow TextServicesManagerService to disconnect the bound spell 164 * checker. 165 */ close()166 public void close() { 167 mGuard.close(); 168 mSpellCheckerSessionListenerImpl.close(); 169 mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl); 170 } 171 172 /** 173 * Get suggestions from the specified sentences 174 * @param textInfos an array of text metadata for a spell checker 175 * @param suggestionsLimit the maximum number of suggestions that will be returned 176 */ getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit)177 public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) { 178 mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple( 179 textInfos, suggestionsLimit); 180 } 181 182 /** 183 * Get candidate strings for a substring of the specified text. 184 * @param textInfo text metadata for a spell checker 185 * @param suggestionsLimit the maximum number of suggestions that will be returned 186 * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead 187 */ 188 @Deprecated getSuggestions(TextInfo textInfo, int suggestionsLimit)189 public void getSuggestions(TextInfo textInfo, int suggestionsLimit) { 190 getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false); 191 } 192 193 /** 194 * A batch process of getSuggestions 195 * @param textInfos an array of text metadata for a spell checker 196 * @param suggestionsLimit the maximum number of suggestions that will be returned 197 * @param sequentialWords true if textInfos can be treated as sequential words. 198 * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead 199 */ 200 @Deprecated getSuggestions( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)201 public void getSuggestions( 202 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 203 if (DBG) { 204 Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId()); 205 } 206 mSpellCheckerSessionListenerImpl.getSuggestionsMultiple( 207 textInfos, suggestionsLimit, sequentialWords); 208 } 209 handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos)210 private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) { 211 mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); 212 } 213 handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos)214 private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) { 215 mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos); 216 } 217 218 private static final class SpellCheckerSessionListenerImpl 219 extends ISpellCheckerSessionListener.Stub { 220 private static final int TASK_CANCEL = 1; 221 private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; 222 private static final int TASK_CLOSE = 3; 223 private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4; taskToString(int task)224 private static String taskToString(int task) { 225 switch (task) { 226 case TASK_CANCEL: 227 return "TASK_CANCEL"; 228 case TASK_GET_SUGGESTIONS_MULTIPLE: 229 return "TASK_GET_SUGGESTIONS_MULTIPLE"; 230 case TASK_CLOSE: 231 return "TASK_CLOSE"; 232 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: 233 return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE"; 234 default: 235 return "Unexpected task=" + task; 236 } 237 } 238 239 private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<>(); 240 private Handler mHandler; 241 242 private static final int STATE_WAIT_CONNECTION = 0; 243 private static final int STATE_CONNECTED = 1; 244 private static final int STATE_CLOSED_AFTER_CONNECTION = 2; 245 private static final int STATE_CLOSED_BEFORE_CONNECTION = 3; stateToString(int state)246 private static String stateToString(int state) { 247 switch (state) { 248 case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION"; 249 case STATE_CONNECTED: return "STATE_CONNECTED"; 250 case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION"; 251 case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION"; 252 default: return "Unexpected state=" + state; 253 } 254 } 255 private int mState = STATE_WAIT_CONNECTION; 256 257 private ISpellCheckerSession mISpellCheckerSession; 258 private HandlerThread mThread; 259 private Handler mAsyncHandler; 260 SpellCheckerSessionListenerImpl(Handler handler)261 public SpellCheckerSessionListenerImpl(Handler handler) { 262 mHandler = handler; 263 } 264 265 private static class SpellCheckerParams { 266 public final int mWhat; 267 public final TextInfo[] mTextInfos; 268 public final int mSuggestionsLimit; 269 public final boolean mSequentialWords; 270 public ISpellCheckerSession mSession; SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)271 public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, 272 boolean sequentialWords) { 273 mWhat = what; 274 mTextInfos = textInfos; 275 mSuggestionsLimit = suggestionsLimit; 276 mSequentialWords = sequentialWords; 277 } 278 } 279 processTask(ISpellCheckerSession session, SpellCheckerParams scp, boolean async)280 private void processTask(ISpellCheckerSession session, SpellCheckerParams scp, 281 boolean async) { 282 if (DBG) { 283 synchronized (this) { 284 Log.d(TAG, "entering processTask:" 285 + " session.hashCode()=#" + Integer.toHexString(session.hashCode()) 286 + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async 287 + " mAsyncHandler=" + mAsyncHandler 288 + " mState=" + stateToString(mState)); 289 } 290 } 291 if (async || mAsyncHandler == null) { 292 switch (scp.mWhat) { 293 case TASK_CANCEL: 294 try { 295 session.onCancel(); 296 } catch (RemoteException e) { 297 Log.e(TAG, "Failed to cancel " + e); 298 } 299 break; 300 case TASK_GET_SUGGESTIONS_MULTIPLE: 301 try { 302 session.onGetSuggestionsMultiple(scp.mTextInfos, 303 scp.mSuggestionsLimit, scp.mSequentialWords); 304 } catch (RemoteException e) { 305 Log.e(TAG, "Failed to get suggestions " + e); 306 } 307 break; 308 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: 309 try { 310 session.onGetSentenceSuggestionsMultiple( 311 scp.mTextInfos, scp.mSuggestionsLimit); 312 } catch (RemoteException e) { 313 Log.e(TAG, "Failed to get suggestions " + e); 314 } 315 break; 316 case TASK_CLOSE: 317 try { 318 session.onClose(); 319 } catch (RemoteException e) { 320 Log.e(TAG, "Failed to close " + e); 321 } 322 break; 323 } 324 } else { 325 // The interface is to a local object, so need to execute it 326 // asynchronously. 327 scp.mSession = session; 328 mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp)); 329 } 330 331 if (scp.mWhat == TASK_CLOSE) { 332 // If we are closing, we want to clean up our state now even 333 // if it is pending as an async operation. 334 synchronized (this) { 335 processCloseLocked(); 336 } 337 } 338 } 339 processCloseLocked()340 private void processCloseLocked() { 341 if (DBG) Log.d(TAG, "entering processCloseLocked:" 342 + " session" + (mISpellCheckerSession != null ? ".hashCode()=#" 343 + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null") 344 + " mState=" + stateToString(mState)); 345 mISpellCheckerSession = null; 346 if (mThread != null) { 347 mThread.quit(); 348 } 349 mHandler = null; 350 mPendingTasks.clear(); 351 mThread = null; 352 mAsyncHandler = null; 353 switch (mState) { 354 case STATE_WAIT_CONNECTION: 355 mState = STATE_CLOSED_BEFORE_CONNECTION; 356 break; 357 case STATE_CONNECTED: 358 mState = STATE_CLOSED_AFTER_CONNECTION; 359 break; 360 default: 361 Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" + 362 stateToString(mState)); 363 break; 364 } 365 } 366 onServiceConnected(ISpellCheckerSession session)367 public void onServiceConnected(ISpellCheckerSession session) { 368 synchronized (this) { 369 switch (mState) { 370 case STATE_WAIT_CONNECTION: 371 // OK, go ahead. 372 break; 373 case STATE_CLOSED_BEFORE_CONNECTION: 374 // This is possible, and not an error. The client no longer is interested 375 // in this connection. OK to ignore. 376 if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is" 377 + " already closed."); 378 return; 379 default: 380 Log.e(TAG, "ignoring onServiceConnected due to unexpected mState=" 381 + stateToString(mState)); 382 return; 383 } 384 if (session == null) { 385 Log.e(TAG, "ignoring onServiceConnected due to session=null"); 386 return; 387 } 388 mISpellCheckerSession = session; 389 if (session.asBinder() instanceof Binder && mThread == null) { 390 if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected."); 391 // If this is a local object, we need to do our own threading 392 // to make sure we handle it asynchronously. 393 mThread = new HandlerThread("SpellCheckerSession", 394 Process.THREAD_PRIORITY_BACKGROUND); 395 mThread.start(); 396 mAsyncHandler = new Handler(mThread.getLooper()) { 397 @Override public void handleMessage(Message msg) { 398 SpellCheckerParams scp = (SpellCheckerParams)msg.obj; 399 processTask(scp.mSession, scp, true); 400 } 401 }; 402 } 403 mState = STATE_CONNECTED; 404 if (DBG) { 405 Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#" 406 + Integer.toHexString(mISpellCheckerSession.hashCode()) 407 + " mPendingTasks.size()=" + mPendingTasks.size()); 408 } 409 while (!mPendingTasks.isEmpty()) { 410 processTask(session, mPendingTasks.poll(), false); 411 } 412 } 413 } 414 cancel()415 public void cancel() { 416 processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false)); 417 } 418 getSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)419 public void getSuggestionsMultiple( 420 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 421 processOrEnqueueTask( 422 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos, 423 suggestionsLimit, sequentialWords)); 424 } 425 getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)426 public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { 427 processOrEnqueueTask( 428 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE, 429 textInfos, suggestionsLimit, false)); 430 } 431 close()432 public void close() { 433 processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false)); 434 } 435 isDisconnected()436 public boolean isDisconnected() { 437 synchronized (this) { 438 return mState != STATE_CONNECTED; 439 } 440 } 441 processOrEnqueueTask(SpellCheckerParams scp)442 private void processOrEnqueueTask(SpellCheckerParams scp) { 443 ISpellCheckerSession session; 444 synchronized (this) { 445 if (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION 446 || mState == STATE_CLOSED_BEFORE_CONNECTION)) { 447 // It is OK to call SpellCheckerSession#close() multiple times. 448 // Don't output confusing/misleading warning messages. 449 return; 450 } 451 if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) { 452 Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState=" 453 + stateToString(mState) 454 + " scp.mWhat=" + taskToString(scp.mWhat)); 455 return; 456 } 457 458 if (mState == STATE_WAIT_CONNECTION) { 459 // If we are still waiting for the connection. Need to pay special attention. 460 if (scp.mWhat == TASK_CLOSE) { 461 processCloseLocked(); 462 return; 463 } 464 // Enqueue the task to task queue. 465 SpellCheckerParams closeTask = null; 466 if (scp.mWhat == TASK_CANCEL) { 467 if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask."); 468 while (!mPendingTasks.isEmpty()) { 469 final SpellCheckerParams tmp = mPendingTasks.poll(); 470 if (tmp.mWhat == TASK_CLOSE) { 471 // Only one close task should be processed, while we need to remove 472 // all close tasks from the queue 473 closeTask = tmp; 474 } 475 } 476 } 477 mPendingTasks.offer(scp); 478 if (closeTask != null) { 479 mPendingTasks.offer(closeTask); 480 } 481 if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the" 482 + " connection is not established." 483 + " mPendingTasks.size()=" + mPendingTasks.size()); 484 return; 485 } 486 487 session = mISpellCheckerSession; 488 } 489 // session must never be null here. 490 processTask(session, scp, false); 491 } 492 493 @Override onGetSuggestions(SuggestionsInfo[] results)494 public void onGetSuggestions(SuggestionsInfo[] results) { 495 synchronized (this) { 496 if (mHandler != null) { 497 mHandler.sendMessage(Message.obtain(mHandler, 498 MSG_ON_GET_SUGGESTION_MULTIPLE, results)); 499 } 500 } 501 } 502 503 @Override onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)504 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { 505 synchronized (this) { 506 if (mHandler != null) { 507 mHandler.sendMessage(Message.obtain(mHandler, 508 MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); 509 } 510 } 511 } 512 } 513 514 /** 515 * Callback for getting results from text services 516 */ 517 public interface SpellCheckerSessionListener { 518 /** 519 * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)} 520 * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 521 * @param results an array of {@link SuggestionsInfo}s. 522 * These results are suggestions for {@link TextInfo}s queried by 523 * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or 524 * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 525 */ onGetSuggestions(SuggestionsInfo[] results)526 public void onGetSuggestions(SuggestionsInfo[] results); 527 /** 528 * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} 529 * @param results an array of {@link SentenceSuggestionsInfo}s. 530 * These results are suggestions for {@link TextInfo}s 531 * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}. 532 */ onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)533 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results); 534 } 535 536 private static final class InternalListener extends ITextServicesSessionListener.Stub { 537 private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl; 538 InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl)539 public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) { 540 mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl; 541 } 542 543 @Override onServiceConnected(ISpellCheckerSession session)544 public void onServiceConnected(ISpellCheckerSession session) { 545 mParentSpellCheckerSessionListenerImpl.onServiceConnected(session); 546 } 547 } 548 549 @Override finalize()550 protected void finalize() throws Throwable { 551 try { 552 // Note that mGuard will be null if the constructor threw. 553 if (mGuard != null) { 554 mGuard.warnIfOpen(); 555 close(); 556 } 557 } finally { 558 super.finalize(); 559 } 560 } 561 562 /** 563 * @hide 564 */ getTextServicesSessionListener()565 public ITextServicesSessionListener getTextServicesSessionListener() { 566 return mInternalListener; 567 } 568 569 /** 570 * @hide 571 */ getSpellCheckerSessionListener()572 public ISpellCheckerSessionListener getSpellCheckerSessionListener() { 573 return mSpellCheckerSessionListenerImpl; 574 } 575 } 576