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