SpellCheckerSession.java revision 0dc1f648a09b46c45190ba1ce7daecf7fada4347
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.Handler; 25import android.os.Message; 26import android.os.RemoteException; 27import android.util.Log; 28import android.view.textservice.SpellCheckerInfo; 29import android.view.textservice.SuggestionsInfo; 30import android.view.textservice.TextInfo; 31 32import java.util.LinkedList; 33import 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 */ 83public 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 private static final String SUPPORT_SENTENCE_SPELL_CHECK = "SupportSentenceSpellCheck"; 92 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 SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; 101 private final SpellCheckerSubtype mSubtype; 102 103 private boolean mIsUsed; 104 private SpellCheckerSessionListener mSpellCheckerSessionListener; 105 106 /** Handler that will execute the main tasks */ 107 private final Handler mHandler = new Handler() { 108 @Override 109 public void handleMessage(Message msg) { 110 switch (msg.what) { 111 case MSG_ON_GET_SUGGESTION_MULTIPLE: 112 handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); 113 break; 114 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE: 115 handleOnGetSuggestionsMultipleForSentence((SuggestionsInfo[]) msg.obj); 116 break; 117 } 118 } 119 }; 120 121 /** 122 * Constructor 123 * @hide 124 */ 125 public SpellCheckerSession( 126 SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener, 127 SpellCheckerSubtype subtype) { 128 if (info == null || listener == null || tsm == null) { 129 throw new NullPointerException(); 130 } 131 mSpellCheckerInfo = info; 132 mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler); 133 mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl); 134 mTextServicesManager = tsm; 135 mIsUsed = true; 136 mSpellCheckerSessionListener = listener; 137 mSubtype = subtype; 138 } 139 140 /** 141 * @return true if the connection to a text service of this session is disconnected and not 142 * alive. 143 */ 144 public boolean isSessionDisconnected() { 145 return mSpellCheckerSessionListenerImpl.isDisconnected(); 146 } 147 148 /** 149 * Get the spell checker service info this spell checker session has. 150 * @return SpellCheckerInfo for the specified locale. 151 */ 152 public SpellCheckerInfo getSpellChecker() { 153 return mSpellCheckerInfo; 154 } 155 156 /** 157 * Cancel pending and running spell check tasks 158 */ 159 public void cancel() { 160 mSpellCheckerSessionListenerImpl.cancel(); 161 } 162 163 /** 164 * Finish this session and allow TextServicesManagerService to disconnect the bound spell 165 * checker. 166 */ 167 public void close() { 168 mIsUsed = false; 169 try { 170 mSpellCheckerSessionListenerImpl.close(); 171 mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl); 172 } catch (RemoteException e) { 173 // do nothing 174 } 175 } 176 177 /** 178 * @hide 179 */ 180 public void getSuggestionsForSentence(TextInfo textInfo, int suggestionsLimit) { 181 mSpellCheckerSessionListenerImpl.getSuggestionsMultipleForSentence( 182 new TextInfo[] {textInfo}, 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 number of limit of suggestions returned 189 */ 190 public void getSuggestions(TextInfo textInfo, int suggestionsLimit) { 191 getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false); 192 } 193 194 /** 195 * A batch process of getSuggestions 196 * @param textInfos an array of text metadata for a spell checker 197 * @param suggestionsLimit the number of limit of suggestions returned 198 * @param sequentialWords true if textInfos can be treated as sequential words. 199 */ 200 public void getSuggestions( 201 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 202 if (DBG) { 203 Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId()); 204 } 205 // TODO: Handle multiple words suggestions by using WordBreakIterator 206 mSpellCheckerSessionListenerImpl.getSuggestionsMultiple( 207 textInfos, suggestionsLimit, sequentialWords); 208 } 209 210 private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) { 211 mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); 212 } 213 214 private void handleOnGetSuggestionsMultipleForSentence(SuggestionsInfo[] suggestionInfos) { 215 mSpellCheckerSessionListener.onGetSuggestionsForSentence(suggestionInfos); 216 } 217 218 private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub { 219 private static final int TASK_CANCEL = 1; 220 private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; 221 private static final int TASK_CLOSE = 3; 222 private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4; 223 private final Queue<SpellCheckerParams> mPendingTasks = 224 new LinkedList<SpellCheckerParams>(); 225 private Handler mHandler; 226 227 private boolean mOpened; 228 private ISpellCheckerSession mISpellCheckerSession; 229 230 public SpellCheckerSessionListenerImpl(Handler handler) { 231 mOpened = false; 232 mHandler = handler; 233 } 234 235 private static class SpellCheckerParams { 236 public final int mWhat; 237 public final TextInfo[] mTextInfos; 238 public final int mSuggestionsLimit; 239 public final boolean mSequentialWords; 240 public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, 241 boolean sequentialWords) { 242 mWhat = what; 243 mTextInfos = textInfos; 244 mSuggestionsLimit = suggestionsLimit; 245 mSequentialWords = sequentialWords; 246 } 247 } 248 249 private void processTask(SpellCheckerParams scp) { 250 switch (scp.mWhat) { 251 case TASK_CANCEL: 252 processCancel(); 253 break; 254 case TASK_GET_SUGGESTIONS_MULTIPLE: 255 processGetSuggestionsMultiple(scp); 256 break; 257 case TASK_CLOSE: 258 processClose(); 259 break; 260 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: 261 processGetSuggestionsMultipleForSentence(scp); 262 break; 263 } 264 } 265 266 public synchronized void onServiceConnected(ISpellCheckerSession session) { 267 mISpellCheckerSession = session; 268 mOpened = true; 269 if (DBG) 270 Log.d(TAG, "onServiceConnected - Success"); 271 while (!mPendingTasks.isEmpty()) { 272 processTask(mPendingTasks.poll()); 273 } 274 } 275 276 public void cancel() { 277 if (DBG) { 278 Log.w(TAG, "cancel"); 279 } 280 processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false)); 281 } 282 283 public void getSuggestionsMultiple( 284 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { 285 if (DBG) { 286 Log.w(TAG, "getSuggestionsMultiple"); 287 } 288 processOrEnqueueTask( 289 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos, 290 suggestionsLimit, sequentialWords)); 291 } 292 293 public void getSuggestionsMultipleForSentence(TextInfo[] textInfos, int suggestionsLimit) { 294 if (DBG) { 295 Log.w(TAG, "getSuggestionsMultipleForSentence"); 296 } 297 processOrEnqueueTask( 298 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE, 299 textInfos, suggestionsLimit, false)); 300 } 301 302 public void close() { 303 if (DBG) { 304 Log.w(TAG, "close"); 305 } 306 processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false)); 307 } 308 309 public boolean isDisconnected() { 310 return mOpened && mISpellCheckerSession == null; 311 } 312 313 public boolean checkOpenConnection() { 314 if (mISpellCheckerSession != null) { 315 return true; 316 } 317 Log.e(TAG, "not connected to the spellchecker service."); 318 return false; 319 } 320 321 private void processOrEnqueueTask(SpellCheckerParams scp) { 322 if (DBG) { 323 Log.d(TAG, "process or enqueue task: " + mISpellCheckerSession); 324 } 325 SpellCheckerParams closeTask = null; 326 if (mISpellCheckerSession == null) { 327 if (scp.mWhat == TASK_CANCEL) { 328 while (!mPendingTasks.isEmpty()) { 329 final SpellCheckerParams tmp = mPendingTasks.poll(); 330 if (tmp.mWhat == TASK_CLOSE) { 331 // Only one close task should be processed, while we need to remove all 332 // close tasks from the queue 333 closeTask = tmp; 334 } 335 } 336 } 337 mPendingTasks.offer(scp); 338 if (closeTask != null) { 339 mPendingTasks.offer(closeTask); 340 } 341 } else { 342 processTask(scp); 343 } 344 } 345 346 private void processCancel() { 347 if (!checkOpenConnection()) { 348 return; 349 } 350 if (DBG) { 351 Log.w(TAG, "Cancel spell checker tasks."); 352 } 353 try { 354 mISpellCheckerSession.onCancel(); 355 } catch (RemoteException e) { 356 Log.e(TAG, "Failed to cancel " + e); 357 } 358 } 359 360 private void processClose() { 361 if (!checkOpenConnection()) { 362 return; 363 } 364 if (DBG) { 365 Log.w(TAG, "Close spell checker tasks."); 366 } 367 try { 368 mISpellCheckerSession.onClose(); 369 mISpellCheckerSession = null; 370 mHandler = null; 371 } catch (RemoteException e) { 372 Log.e(TAG, "Failed to close " + e); 373 } 374 } 375 376 private void processGetSuggestionsMultiple(SpellCheckerParams scp) { 377 if (!checkOpenConnection()) { 378 return; 379 } 380 if (DBG) { 381 Log.w(TAG, "Get suggestions from the spell checker."); 382 } 383 try { 384 mISpellCheckerSession.onGetSuggestionsMultiple( 385 scp.mTextInfos, scp.mSuggestionsLimit, scp.mSequentialWords); 386 } catch (RemoteException e) { 387 Log.e(TAG, "Failed to get suggestions " + e); 388 } 389 } 390 391 private void processGetSuggestionsMultipleForSentence(SpellCheckerParams scp) { 392 if (!checkOpenConnection()) { 393 return; 394 } 395 if (DBG) { 396 Log.w(TAG, "Get suggestions from the spell checker."); 397 } 398 if (scp.mTextInfos.length != 1) { 399 throw new IllegalArgumentException(); 400 } 401 try { 402 mISpellCheckerSession.onGetSuggestionsMultipleForSentence( 403 scp.mTextInfos, scp.mSuggestionsLimit); 404 } catch (RemoteException e) { 405 Log.e(TAG, "Failed to get suggestions " + e); 406 } 407 } 408 409 @Override 410 public void onGetSuggestions(SuggestionsInfo[] results) { 411 mHandler.sendMessage(Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE, results)); 412 } 413 414 @Override 415 public void onGetSuggestionsForSentence(SuggestionsInfo[] results) { 416 mHandler.sendMessage( 417 Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); 418 } 419 } 420 421 /** 422 * Callback for getting results from text services 423 */ 424 public interface SpellCheckerSessionListener { 425 /** 426 * Callback for "getSuggestions" 427 * @param results an array of results of getSuggestions 428 */ 429 public void onGetSuggestions(SuggestionsInfo[] results); 430 /** 431 * @hide 432 */ 433 public void onGetSuggestionsForSentence(SuggestionsInfo[] results); 434 } 435 436 private static class InternalListener extends ITextServicesSessionListener.Stub { 437 private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl; 438 439 public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) { 440 mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl; 441 } 442 443 @Override 444 public void onServiceConnected(ISpellCheckerSession session) { 445 if (DBG) { 446 Log.w(TAG, "SpellCheckerSession connected."); 447 } 448 mParentSpellCheckerSessionListenerImpl.onServiceConnected(session); 449 } 450 } 451 452 @Override 453 protected void finalize() throws Throwable { 454 super.finalize(); 455 if (mIsUsed) { 456 Log.e(TAG, "SpellCheckerSession was not finished properly." + 457 "You should call finishShession() when you finished to use a spell checker."); 458 close(); 459 } 460 } 461 462 /** 463 * @hide 464 */ 465 public ITextServicesSessionListener getTextServicesSessionListener() { 466 return mInternalListener; 467 } 468 469 /** 470 * @hide 471 */ 472 public ISpellCheckerSessionListener getSpellCheckerSessionListener() { 473 return mSpellCheckerSessionListenerImpl; 474 } 475 476 /** 477 * @hide 478 */ 479 public boolean isSentenceSpellCheckSupported() { 480 return mSubtype.containsExtraValueKey(SUPPORT_SENTENCE_SPELL_CHECK); 481 } 482} 483