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