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.os.Binder; 20 import android.os.Handler; 21 import android.os.HandlerThread; 22 import android.os.Message; 23 import android.os.Process; 24 import android.os.RemoteException; 25 import android.util.Log; 26 27 import com.android.internal.textservice.ISpellCheckerSession; 28 import com.android.internal.textservice.ISpellCheckerSessionListener; 29 import com.android.internal.textservice.ITextServicesManager; 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 ITextServicesManager mTextServicesManager; 99 private final SpellCheckerInfo mSpellCheckerInfo; 100 private final SpellCheckerSessionListener mSpellCheckerSessionListener; 101 private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; 102 103 private final CloseGuard mGuard = CloseGuard.get(); 104 105 /** Handler that will execute the main tasks */ 106 private final Handler mHandler = new Handler() { 107 @Override 108 public void handleMessage(Message msg) { 109 switch (msg.what) { 110 case MSG_ON_GET_SUGGESTION_MULTIPLE: 111 handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); 112 break; 113 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE: 114 handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj); 115 break; 116 } 117 } 118 }; 119 120 /** 121 * Constructor 122 * @hide 123 */ SpellCheckerSession( SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener)124 public SpellCheckerSession( 125 SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) { 126 if (info == null || listener == null || tsm == null) { 127 throw new NullPointerException(); 128 } 129 mSpellCheckerInfo = info; 130 mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler); 131 mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl); 132 mTextServicesManager = tsm; 133 mSpellCheckerSessionListener = listener; 134 135 mGuard.open("finishSession"); 136 } 137 138 /** 139 * @return true if the connection to a text service of this session is disconnected and not 140 * alive. 141 */ isSessionDisconnected()142 public boolean isSessionDisconnected() { 143 return mSpellCheckerSessionListenerImpl.isDisconnected(); 144 } 145 146 /** 147 * Get the spell checker service info this spell checker session has. 148 * @return SpellCheckerInfo for the specified locale. 149 */ getSpellChecker()150 public SpellCheckerInfo getSpellChecker() { 151 return mSpellCheckerInfo; 152 } 153 154 /** 155 * Cancel pending and running spell check tasks 156 */ cancel()157 public void cancel() { 158 mSpellCheckerSessionListenerImpl.cancel(); 159 } 160 161 /** 162 * Finish this session and allow TextServicesManagerService to disconnect the bound spell 163 * checker. 164 */ close()165 public void close() { 166 mGuard.close(); 167 try { 168 mSpellCheckerSessionListenerImpl.close(); 169 mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl); 170 } catch (RemoteException e) { 171 // do nothing 172 } 173 } 174 175 /** 176 * Get suggestions from the specified sentences 177 * @param textInfos an array of text metadata for a spell checker 178 * @param suggestionsLimit the maximum number of suggestions that will be returned 179 */ getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit)180 public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) { 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 mSpellCheckerSessionListenerImpl.getSuggestionsMultiple( 210 textInfos, suggestionsLimit, sequentialWords); 211 } 212 handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos)213 private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) { 214 mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); 215 } 216 handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos)217 private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) { 218 mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos); 219 } 220 221 private static final class SpellCheckerSessionListenerImpl 222 extends ISpellCheckerSessionListener.Stub { 223 private static final int TASK_CANCEL = 1; 224 private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; 225 private static final int TASK_CLOSE = 3; 226 private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4; taskToString(int task)227 private static String taskToString(int task) { 228 switch (task) { 229 case TASK_CANCEL: 230 return "TASK_CANCEL"; 231 case TASK_GET_SUGGESTIONS_MULTIPLE: 232 return "TASK_GET_SUGGESTIONS_MULTIPLE"; 233 case TASK_CLOSE: 234 return "TASK_CLOSE"; 235 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: 236 return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE"; 237 default: 238 return "Unexpected task=" + task; 239 } 240 } 241 242 private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<>(); 243 private Handler mHandler; 244 245 private static final int STATE_WAIT_CONNECTION = 0; 246 private static final int STATE_CONNECTED = 1; 247 private static final int STATE_CLOSED_AFTER_CONNECTION = 2; 248 private static final int STATE_CLOSED_BEFORE_CONNECTION = 3; stateToString(int state)249 private static String stateToString(int state) { 250 switch (state) { 251 case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION"; 252 case STATE_CONNECTED: return "STATE_CONNECTED"; 253 case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION"; 254 case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION"; 255 default: return "Unexpected state=" + state; 256 } 257 } 258 private int mState = STATE_WAIT_CONNECTION; 259 260 private ISpellCheckerSession mISpellCheckerSession; 261 private HandlerThread mThread; 262 private Handler mAsyncHandler; 263 SpellCheckerSessionListenerImpl(Handler handler)264 public SpellCheckerSessionListenerImpl(Handler handler) { 265 mHandler = handler; 266 } 267 268 private static class SpellCheckerParams { 269 public final int mWhat; 270 public final TextInfo[] mTextInfos; 271 public final int mSuggestionsLimit; 272 public final boolean mSequentialWords; 273 public ISpellCheckerSession mSession; SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)274 public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, 275 boolean sequentialWords) { 276 mWhat = what; 277 mTextInfos = textInfos; 278 mSuggestionsLimit = suggestionsLimit; 279 mSequentialWords = sequentialWords; 280 } 281 } 282 processTask(ISpellCheckerSession session, SpellCheckerParams scp, boolean async)283 private void processTask(ISpellCheckerSession session, SpellCheckerParams scp, 284 boolean async) { 285 if (DBG) { 286 synchronized (this) { 287 Log.d(TAG, "entering processTask:" 288 + " session.hashCode()=#" + Integer.toHexString(session.hashCode()) 289 + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async 290 + " mAsyncHandler=" + mAsyncHandler 291 + " mState=" + stateToString(mState)); 292 } 293 } 294 if (async || mAsyncHandler == null) { 295 switch (scp.mWhat) { 296 case TASK_CANCEL: 297 try { 298 session.onCancel(); 299 } catch (RemoteException e) { 300 Log.e(TAG, "Failed to cancel " + e); 301 } 302 break; 303 case TASK_GET_SUGGESTIONS_MULTIPLE: 304 try { 305 session.onGetSuggestionsMultiple(scp.mTextInfos, 306 scp.mSuggestionsLimit, scp.mSequentialWords); 307 } catch (RemoteException e) { 308 Log.e(TAG, "Failed to get suggestions " + e); 309 } 310 break; 311 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: 312 try { 313 session.onGetSentenceSuggestionsMultiple( 314 scp.mTextInfos, scp.mSuggestionsLimit); 315 } catch (RemoteException e) { 316 Log.e(TAG, "Failed to get suggestions " + e); 317 } 318 break; 319 case TASK_CLOSE: 320 try { 321 session.onClose(); 322 } catch (RemoteException e) { 323 Log.e(TAG, "Failed to close " + e); 324 } 325 break; 326 } 327 } else { 328 // The interface is to a local object, so need to execute it 329 // asynchronously. 330 scp.mSession = session; 331 mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp)); 332 } 333 334 if (scp.mWhat == TASK_CLOSE) { 335 // If we are closing, we want to clean up our state now even 336 // if it is pending as an async operation. 337 synchronized (this) { 338 processCloseLocked(); 339 } 340 } 341 } 342 processCloseLocked()343 private void processCloseLocked() { 344 if (DBG) Log.d(TAG, "entering processCloseLocked:" 345 + " session" + (mISpellCheckerSession != null ? ".hashCode()=#" 346 + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null") 347 + " mState=" + stateToString(mState)); 348 mISpellCheckerSession = null; 349 if (mThread != null) { 350 mThread.quit(); 351 } 352 mHandler = null; 353 mPendingTasks.clear(); 354 mThread = null; 355 mAsyncHandler = null; 356 switch (mState) { 357 case STATE_WAIT_CONNECTION: 358 mState = STATE_CLOSED_BEFORE_CONNECTION; 359 break; 360 case STATE_CONNECTED: 361 mState = STATE_CLOSED_AFTER_CONNECTION; 362 break; 363 default: 364 Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" + 365 stateToString(mState)); 366 break; 367 } 368 } 369 onServiceConnected(ISpellCheckerSession session)370 public void onServiceConnected(ISpellCheckerSession session) { 371 synchronized (this) { 372 switch (mState) { 373 case STATE_WAIT_CONNECTION: 374 // OK, go ahead. 375 break; 376 case STATE_CLOSED_BEFORE_CONNECTION: 377 // This is possible, and not an error. The client no longer is interested 378 // in this connection. OK to ignore. 379 if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is" 380 + " already closed."); 381 return; 382 default: 383 Log.e(TAG, "ignoring onServiceConnected due to unexpected mState=" 384 + stateToString(mState)); 385 return; 386 } 387 if (session == null) { 388 Log.e(TAG, "ignoring onServiceConnected due to session=null"); 389 return; 390 } 391 mISpellCheckerSession = session; 392 if (session.asBinder() instanceof Binder && mThread == null) { 393 if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected."); 394 // If this is a local object, we need to do our own threading 395 // to make sure we handle it asynchronously. 396 mThread = new HandlerThread("SpellCheckerSession", 397 Process.THREAD_PRIORITY_BACKGROUND); 398 mThread.start(); 399 mAsyncHandler = new Handler(mThread.getLooper()) { 400 @Override public void handleMessage(Message msg) { 401 SpellCheckerParams scp = (SpellCheckerParams)msg.obj; 402 processTask(scp.mSession, scp, true); 403 } 404 }; 405 } 406 mState = STATE_CONNECTED; 407 if (DBG) { 408 Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#" 409 + Integer.toHexString(mISpellCheckerSession.hashCode()) 410 + " mPendingTasks.size()=" + mPendingTasks.size()); 411 } 412 while (!mPendingTasks.isEmpty()) { 413 processTask(session, mPendingTasks.poll(), false); 414 } 415 } 416 } 417 cancel()418 public void cancel() { 419 processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false)); 420 } 421 getSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)422 public void getSuggestionsMultiple( 423 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 424 processOrEnqueueTask( 425 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos, 426 suggestionsLimit, sequentialWords)); 427 } 428 getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)429 public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { 430 processOrEnqueueTask( 431 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE, 432 textInfos, suggestionsLimit, false)); 433 } 434 close()435 public void close() { 436 processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false)); 437 } 438 isDisconnected()439 public boolean isDisconnected() { 440 synchronized (this) { 441 return mState != STATE_CONNECTED; 442 } 443 } 444 processOrEnqueueTask(SpellCheckerParams scp)445 private void processOrEnqueueTask(SpellCheckerParams scp) { 446 ISpellCheckerSession session; 447 synchronized (this) { 448 if (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION 449 || mState == STATE_CLOSED_BEFORE_CONNECTION)) { 450 // It is OK to call SpellCheckerSession#close() multiple times. 451 // Don't output confusing/misleading warning messages. 452 return; 453 } 454 if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) { 455 Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState=" 456 + stateToString(mState) 457 + " scp.mWhat=" + taskToString(scp.mWhat)); 458 return; 459 } 460 461 if (mState == STATE_WAIT_CONNECTION) { 462 // If we are still waiting for the connection. Need to pay special attention. 463 if (scp.mWhat == TASK_CLOSE) { 464 processCloseLocked(); 465 return; 466 } 467 // Enqueue the task to task queue. 468 SpellCheckerParams closeTask = null; 469 if (scp.mWhat == TASK_CANCEL) { 470 if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask."); 471 while (!mPendingTasks.isEmpty()) { 472 final SpellCheckerParams tmp = mPendingTasks.poll(); 473 if (tmp.mWhat == TASK_CLOSE) { 474 // Only one close task should be processed, while we need to remove 475 // all close tasks from the queue 476 closeTask = tmp; 477 } 478 } 479 } 480 mPendingTasks.offer(scp); 481 if (closeTask != null) { 482 mPendingTasks.offer(closeTask); 483 } 484 if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the" 485 + " connection is not established." 486 + " mPendingTasks.size()=" + mPendingTasks.size()); 487 return; 488 } 489 490 session = mISpellCheckerSession; 491 } 492 // session must never be null here. 493 processTask(session, scp, false); 494 } 495 496 @Override onGetSuggestions(SuggestionsInfo[] results)497 public void onGetSuggestions(SuggestionsInfo[] results) { 498 synchronized (this) { 499 if (mHandler != null) { 500 mHandler.sendMessage(Message.obtain(mHandler, 501 MSG_ON_GET_SUGGESTION_MULTIPLE, results)); 502 } 503 } 504 } 505 506 @Override onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)507 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { 508 synchronized (this) { 509 if (mHandler != null) { 510 mHandler.sendMessage(Message.obtain(mHandler, 511 MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); 512 } 513 } 514 } 515 } 516 517 /** 518 * Callback for getting results from text services 519 */ 520 public interface SpellCheckerSessionListener { 521 /** 522 * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)} 523 * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 524 * @param results an array of {@link SuggestionsInfo}s. 525 * These results are suggestions for {@link TextInfo}s queried by 526 * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or 527 * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 528 */ onGetSuggestions(SuggestionsInfo[] results)529 public void onGetSuggestions(SuggestionsInfo[] results); 530 /** 531 * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} 532 * @param results an array of {@link SentenceSuggestionsInfo}s. 533 * These results are suggestions for {@link TextInfo}s 534 * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}. 535 */ onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)536 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results); 537 } 538 539 private static final class InternalListener extends ITextServicesSessionListener.Stub { 540 private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl; 541 InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl)542 public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) { 543 mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl; 544 } 545 546 @Override onServiceConnected(ISpellCheckerSession session)547 public void onServiceConnected(ISpellCheckerSession session) { 548 mParentSpellCheckerSessionListenerImpl.onServiceConnected(session); 549 } 550 } 551 552 @Override finalize()553 protected void finalize() throws Throwable { 554 try { 555 // Note that mGuard will be null if the constructor threw. 556 if (mGuard != null) { 557 mGuard.warnIfOpen(); 558 close(); 559 } 560 } finally { 561 super.finalize(); 562 } 563 } 564 565 /** 566 * @hide 567 */ getTextServicesSessionListener()568 public ITextServicesSessionListener getTextServicesSessionListener() { 569 return mInternalListener; 570 } 571 572 /** 573 * @hide 574 */ getSpellCheckerSessionListener()575 public ISpellCheckerSessionListener getSpellCheckerSessionListener() { 576 return mSpellCheckerSessionListenerImpl; 577 } 578 } 579