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 java.util.LinkedList; 33 import java.util.Queue; 34 35 /** 36 * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService. 37 * 38 * 39 * <a name="Applications"></a> 40 * <h3>Applications</h3> 41 * 42 * <p>In most cases, applications that are using the standard 43 * {@link android.widget.TextView} or its subclasses will have little they need 44 * to do to work well with spell checker services. The main things you need to 45 * be aware of are:</p> 46 * 47 * <ul> 48 * <li> Properly set the {@link android.R.attr#inputType} in your editable 49 * text views, so that the spell checker will have enough context to help the 50 * user in editing text in them. 51 * </ul> 52 * 53 * <p>For the rare people amongst us writing client applications that use the spell checker service 54 * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or 55 * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker 56 * service by yourself.</p> 57 * 58 * <h3>Security</h3> 59 * 60 * <p>There are a lot of security issues associated with spell checkers, 61 * since they could monitor all the text being sent to them 62 * through, for instance, {@link android.widget.TextView}. 63 * The Android spell checker framework also allows 64 * arbitrary third party spell checkers, so care must be taken to restrict their 65 * selection and interactions.</p> 66 * 67 * <p>Here are some key points about the security architecture behind the 68 * spell checker framework:</p> 69 * 70 * <ul> 71 * <li>Only the system is allowed to directly access a spell checker framework's 72 * {@link android.service.textservice.SpellCheckerService} interface, via the 73 * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission. This is 74 * enforced in the system by not binding to a spell checker service that does 75 * not require this permission. 76 * 77 * <li>The user must explicitly enable a new spell checker in settings before 78 * they can be enabled, to confirm with the system that they know about it 79 * and want to make it available for use. 80 * </ul> 81 * 82 */ 83 public class SpellCheckerSession { 84 private static final String TAG = SpellCheckerSession.class.getSimpleName(); 85 private static final boolean DBG = false; 86 /** 87 * Name under which a SpellChecker service component publishes information about itself. 88 * This meta-data must reference an XML resource. 89 **/ 90 public static final String SERVICE_META_DATA = "android.view.textservice.scs"; 91 92 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; 93 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2; 94 95 private final InternalListener mInternalListener; 96 private final ITextServicesManager mTextServicesManager; 97 private final SpellCheckerInfo mSpellCheckerInfo; 98 private final SpellCheckerSessionListener mSpellCheckerSessionListener; 99 private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; 100 101 private boolean mIsUsed; 102 103 /** Handler that will execute the main tasks */ 104 private final Handler mHandler = new Handler() { 105 @Override 106 public void handleMessage(Message msg) { 107 switch (msg.what) { 108 case MSG_ON_GET_SUGGESTION_MULTIPLE: 109 handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); 110 break; 111 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE: 112 handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj); 113 break; 114 } 115 } 116 }; 117 118 /** 119 * Constructor 120 * @hide 121 */ SpellCheckerSession( SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener)122 public SpellCheckerSession( 123 SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) { 124 if (info == null || listener == null || tsm == null) { 125 throw new NullPointerException(); 126 } 127 mSpellCheckerInfo = info; 128 mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler); 129 mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl); 130 mTextServicesManager = tsm; 131 mIsUsed = true; 132 mSpellCheckerSessionListener = listener; 133 } 134 135 /** 136 * @return true if the connection to a text service of this session is disconnected and not 137 * alive. 138 */ isSessionDisconnected()139 public boolean isSessionDisconnected() { 140 return mSpellCheckerSessionListenerImpl.isDisconnected(); 141 } 142 143 /** 144 * Get the spell checker service info this spell checker session has. 145 * @return SpellCheckerInfo for the specified locale. 146 */ getSpellChecker()147 public SpellCheckerInfo getSpellChecker() { 148 return mSpellCheckerInfo; 149 } 150 151 /** 152 * Cancel pending and running spell check tasks 153 */ cancel()154 public void cancel() { 155 mSpellCheckerSessionListenerImpl.cancel(); 156 } 157 158 /** 159 * Finish this session and allow TextServicesManagerService to disconnect the bound spell 160 * checker. 161 */ close()162 public void close() { 163 mIsUsed = false; 164 try { 165 mSpellCheckerSessionListenerImpl.close(); 166 mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl); 167 } catch (RemoteException e) { 168 // do nothing 169 } 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 (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) { 446 Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState=" 447 + taskToString(scp.mWhat) 448 + " scp.mWhat=" + taskToString(scp.mWhat)); 449 return; 450 } 451 452 if (mState == STATE_WAIT_CONNECTION) { 453 // If we are still waiting for the connection. Need to pay special attention. 454 if (scp.mWhat == TASK_CLOSE) { 455 processCloseLocked(); 456 return; 457 } 458 // Enqueue the task to task queue. 459 SpellCheckerParams closeTask = null; 460 if (scp.mWhat == TASK_CANCEL) { 461 if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask."); 462 while (!mPendingTasks.isEmpty()) { 463 final SpellCheckerParams tmp = mPendingTasks.poll(); 464 if (tmp.mWhat == TASK_CLOSE) { 465 // Only one close task should be processed, while we need to remove 466 // all close tasks from the queue 467 closeTask = tmp; 468 } 469 } 470 } 471 mPendingTasks.offer(scp); 472 if (closeTask != null) { 473 mPendingTasks.offer(closeTask); 474 } 475 if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the" 476 + " connection is not established." 477 + " mPendingTasks.size()=" + mPendingTasks.size()); 478 return; 479 } 480 481 session = mISpellCheckerSession; 482 } 483 // session must never be null here. 484 processTask(session, scp, false); 485 } 486 487 @Override onGetSuggestions(SuggestionsInfo[] results)488 public void onGetSuggestions(SuggestionsInfo[] results) { 489 synchronized (this) { 490 if (mHandler != null) { 491 mHandler.sendMessage(Message.obtain(mHandler, 492 MSG_ON_GET_SUGGESTION_MULTIPLE, results)); 493 } 494 } 495 } 496 497 @Override onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)498 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { 499 synchronized (this) { 500 if (mHandler != null) { 501 mHandler.sendMessage(Message.obtain(mHandler, 502 MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); 503 } 504 } 505 } 506 } 507 508 /** 509 * Callback for getting results from text services 510 */ 511 public interface SpellCheckerSessionListener { 512 /** 513 * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)} 514 * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 515 * @param results an array of {@link SuggestionsInfo}s. 516 * These results are suggestions for {@link TextInfo}s queried by 517 * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or 518 * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 519 */ onGetSuggestions(SuggestionsInfo[] results)520 public void onGetSuggestions(SuggestionsInfo[] results); 521 /** 522 * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} 523 * @param results an array of {@link SentenceSuggestionsInfo}s. 524 * These results are suggestions for {@link TextInfo}s 525 * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}. 526 */ onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)527 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results); 528 } 529 530 private static final class InternalListener extends ITextServicesSessionListener.Stub { 531 private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl; 532 InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl)533 public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) { 534 mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl; 535 } 536 537 @Override onServiceConnected(ISpellCheckerSession session)538 public void onServiceConnected(ISpellCheckerSession session) { 539 mParentSpellCheckerSessionListenerImpl.onServiceConnected(session); 540 } 541 } 542 543 @Override finalize()544 protected void finalize() throws Throwable { 545 super.finalize(); 546 if (mIsUsed) { 547 Log.e(TAG, "SpellCheckerSession was not finished properly." + 548 "You should call finishSession() when you finished to use a spell checker."); 549 close(); 550 } 551 } 552 553 /** 554 * @hide 555 */ getTextServicesSessionListener()556 public ITextServicesSessionListener getTextServicesSessionListener() { 557 return mInternalListener; 558 } 559 560 /** 561 * @hide 562 */ getSpellCheckerSessionListener()563 public ISpellCheckerSessionListener getSpellCheckerSessionListener() { 564 return mSpellCheckerSessionListenerImpl; 565 } 566 } 567