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 import android.view.textservice.SpellCheckerInfo; 32 import android.view.textservice.SuggestionsInfo; 33 import android.view.textservice.TextInfo; 34 35 import java.util.LinkedList; 36 import 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 */ 86 public 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 95 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; 96 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2; 97 98 private final InternalListener mInternalListener; 99 private final ITextServicesManager mTextServicesManager; 100 private final SpellCheckerInfo mSpellCheckerInfo; 101 private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; 102 private final SpellCheckerSubtype mSubtype; 103 104 private boolean mIsUsed; 105 private SpellCheckerSessionListener mSpellCheckerSessionListener; 106 107 /** Handler that will execute the main tasks */ 108 private final Handler mHandler = new Handler() { 109 @Override 110 public void handleMessage(Message msg) { 111 switch (msg.what) { 112 case MSG_ON_GET_SUGGESTION_MULTIPLE: 113 handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); 114 break; 115 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE: 116 handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj); 117 break; 118 } 119 } 120 }; 121 122 /** 123 * Constructor 124 * @hide 125 */ 126 public SpellCheckerSession( 127 SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener, 128 SpellCheckerSubtype subtype) { 129 if (info == null || listener == null || tsm == null) { 130 throw new NullPointerException(); 131 } 132 mSpellCheckerInfo = info; 133 mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler); 134 mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl); 135 mTextServicesManager = tsm; 136 mIsUsed = true; 137 mSpellCheckerSessionListener = listener; 138 mSubtype = subtype; 139 } 140 141 /** 142 * @return true if the connection to a text service of this session is disconnected and not 143 * alive. 144 */ 145 public boolean isSessionDisconnected() { 146 return mSpellCheckerSessionListenerImpl.isDisconnected(); 147 } 148 149 /** 150 * Get the spell checker service info this spell checker session has. 151 * @return SpellCheckerInfo for the specified locale. 152 */ 153 public SpellCheckerInfo getSpellChecker() { 154 return mSpellCheckerInfo; 155 } 156 157 /** 158 * Cancel pending and running spell check tasks 159 */ 160 public void cancel() { 161 mSpellCheckerSessionListenerImpl.cancel(); 162 } 163 164 /** 165 * Finish this session and allow TextServicesManagerService to disconnect the bound spell 166 * checker. 167 */ 168 public void close() { 169 mIsUsed = false; 170 try { 171 mSpellCheckerSessionListenerImpl.close(); 172 mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl); 173 } catch (RemoteException e) { 174 // do nothing 175 } 176 } 177 178 /** 179 * Get suggestions from the specified sentences 180 * @param textInfos an array of text metadata for a spell checker 181 * @param suggestionsLimit the maximum number of suggestions that will be returned 182 */ 183 public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) { 184 mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple( 185 textInfos, 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 maximum number of suggestions that will be returned 192 * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead 193 */ 194 @Deprecated 195 public void getSuggestions(TextInfo textInfo, int suggestionsLimit) { 196 getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false); 197 } 198 199 /** 200 * A batch process of getSuggestions 201 * @param textInfos an array of text metadata for a spell checker 202 * @param suggestionsLimit the maximum number of suggestions that will be returned 203 * @param sequentialWords true if textInfos can be treated as sequential words. 204 * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead 205 */ 206 @Deprecated 207 public void getSuggestions( 208 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 209 if (DBG) { 210 Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId()); 211 } 212 mSpellCheckerSessionListenerImpl.getSuggestionsMultiple( 213 textInfos, suggestionsLimit, sequentialWords); 214 } 215 216 private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) { 217 mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); 218 } 219 220 private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) { 221 mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos); 222 } 223 224 private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub { 225 private static final int TASK_CANCEL = 1; 226 private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; 227 private static final int TASK_CLOSE = 3; 228 private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4; 229 private final Queue<SpellCheckerParams> mPendingTasks = 230 new LinkedList<SpellCheckerParams>(); 231 private Handler mHandler; 232 233 private boolean mOpened; 234 private ISpellCheckerSession mISpellCheckerSession; 235 private HandlerThread mThread; 236 private Handler mAsyncHandler; 237 238 public SpellCheckerSessionListenerImpl(Handler handler) { 239 mOpened = false; 240 mHandler = handler; 241 } 242 243 private static class SpellCheckerParams { 244 public final int mWhat; 245 public final TextInfo[] mTextInfos; 246 public final int mSuggestionsLimit; 247 public final boolean mSequentialWords; 248 public ISpellCheckerSession mSession; 249 public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, 250 boolean sequentialWords) { 251 mWhat = what; 252 mTextInfos = textInfos; 253 mSuggestionsLimit = suggestionsLimit; 254 mSequentialWords = sequentialWords; 255 } 256 } 257 258 private void processTask(ISpellCheckerSession session, SpellCheckerParams scp, 259 boolean async) { 260 if (async || mAsyncHandler == null) { 261 switch (scp.mWhat) { 262 case TASK_CANCEL: 263 if (DBG) { 264 Log.w(TAG, "Cancel spell checker tasks."); 265 } 266 try { 267 session.onCancel(); 268 } catch (RemoteException e) { 269 Log.e(TAG, "Failed to cancel " + e); 270 } 271 break; 272 case TASK_GET_SUGGESTIONS_MULTIPLE: 273 if (DBG) { 274 Log.w(TAG, "Get suggestions from the spell checker."); 275 } 276 try { 277 session.onGetSuggestionsMultiple(scp.mTextInfos, 278 scp.mSuggestionsLimit, scp.mSequentialWords); 279 } catch (RemoteException e) { 280 Log.e(TAG, "Failed to get suggestions " + e); 281 } 282 break; 283 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: 284 if (DBG) { 285 Log.w(TAG, "Get sentence suggestions from the spell checker."); 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 {@link SpellCheckerSession#getSuggestions(TextInfo, int)} 441 * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 442 * @param results an array of {@link SuggestionsInfo}s. 443 * These results are suggestions for {@link TextInfo}s queried by 444 * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or 445 * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} 446 */ 447 public void onGetSuggestions(SuggestionsInfo[] results); 448 /** 449 * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} 450 * @param results an array of {@link SentenceSuggestionsInfo}s. 451 * These results are suggestions for {@link TextInfo}s 452 * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}. 453 */ 454 public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results); 455 } 456 457 private static class InternalListener extends ITextServicesSessionListener.Stub { 458 private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl; 459 460 public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) { 461 mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl; 462 } 463 464 @Override 465 public void onServiceConnected(ISpellCheckerSession session) { 466 if (DBG) { 467 Log.w(TAG, "SpellCheckerSession connected."); 468 } 469 mParentSpellCheckerSessionListenerImpl.onServiceConnected(session); 470 } 471 } 472 473 @Override 474 protected void finalize() throws Throwable { 475 super.finalize(); 476 if (mIsUsed) { 477 Log.e(TAG, "SpellCheckerSession was not finished properly." + 478 "You should call finishShession() when you finished to use a spell checker."); 479 close(); 480 } 481 } 482 483 /** 484 * @hide 485 */ 486 public ITextServicesSessionListener getTextServicesSessionListener() { 487 return mInternalListener; 488 } 489 490 /** 491 * @hide 492 */ 493 public ISpellCheckerSessionListener getSpellCheckerSessionListener() { 494 return mSpellCheckerSessionListenerImpl; 495 } 496 } 497