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 dalvik.system.CloseGuard;
33 
34 import java.util.LinkedList;
35 import java.util.Queue;
36 
37 /**
38  * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService.
39  *
40  *
41  * <a name="Applications"></a>
42  * <h3>Applications</h3>
43  *
44  * <p>In most cases, applications that are using the standard
45  * {@link android.widget.TextView} or its subclasses will have little they need
46  * to do to work well with spell checker services.  The main things you need to
47  * be aware of are:</p>
48  *
49  * <ul>
50  * <li> Properly set the {@link android.R.attr#inputType} in your editable
51  * text views, so that the spell checker will have enough context to help the
52  * user in editing text in them.
53  * </ul>
54  *
55  * <p>For the rare people amongst us writing client applications that use the spell checker service
56  * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or
57  * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker
58  * service by yourself.</p>
59  *
60  * <h3>Security</h3>
61  *
62  * <p>There are a lot of security issues associated with spell checkers,
63  * since they could monitor all the text being sent to them
64  * through, for instance, {@link android.widget.TextView}.
65  * The Android spell checker framework also allows
66  * arbitrary third party spell checkers, so care must be taken to restrict their
67  * selection and interactions.</p>
68  *
69  * <p>Here are some key points about the security architecture behind the
70  * spell checker framework:</p>
71  *
72  * <ul>
73  * <li>Only the system is allowed to directly access a spell checker framework's
74  * {@link android.service.textservice.SpellCheckerService} interface, via the
75  * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission.  This is
76  * enforced in the system by not binding to a spell checker service that does
77  * not require this permission.
78  *
79  * <li>The user must explicitly enable a new spell checker in settings before
80  * they can be enabled, to confirm with the system that they know about it
81  * and want to make it available for use.
82  * </ul>
83  *
84  */
85 public class SpellCheckerSession {
86     private static final String TAG = SpellCheckerSession.class.getSimpleName();
87     private static final boolean DBG = false;
88     /**
89      * Name under which a SpellChecker service component publishes information about itself.
90      * This meta-data must reference an XML resource.
91      **/
92     public static final String SERVICE_META_DATA = "android.view.textservice.scs";
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 SpellCheckerSessionListener mSpellCheckerSessionListener;
101     private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
102 
103     private final CloseGuard mGuard = CloseGuard.get();
104 
105     /** Handler that will execute the main tasks */
106     private final Handler mHandler = new Handler() {
107         @Override
108         public void handleMessage(Message msg) {
109             switch (msg.what) {
110                 case MSG_ON_GET_SUGGESTION_MULTIPLE:
111                     handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj);
112                     break;
113                 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE:
114                     handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj);
115                     break;
116             }
117         }
118     };
119 
120     /**
121      * Constructor
122      * @hide
123      */
SpellCheckerSession( SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener)124     public SpellCheckerSession(
125             SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) {
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         mSpellCheckerSessionListener = listener;
134 
135         mGuard.open("finishSession");
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         mGuard.close();
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 final class SpellCheckerSessionListenerImpl
222             extends ISpellCheckerSessionListener.Stub {
223         private static final int TASK_CANCEL = 1;
224         private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
225         private static final int TASK_CLOSE = 3;
226         private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
taskToString(int task)227         private static String taskToString(int task) {
228             switch (task) {
229                 case TASK_CANCEL:
230                     return "TASK_CANCEL";
231                 case TASK_GET_SUGGESTIONS_MULTIPLE:
232                     return "TASK_GET_SUGGESTIONS_MULTIPLE";
233                 case TASK_CLOSE:
234                     return "TASK_CLOSE";
235                 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
236                     return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE";
237                 default:
238                     return "Unexpected task=" + task;
239             }
240         }
241 
242         private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<>();
243         private Handler mHandler;
244 
245         private static final int STATE_WAIT_CONNECTION = 0;
246         private static final int STATE_CONNECTED = 1;
247         private static final int STATE_CLOSED_AFTER_CONNECTION = 2;
248         private static final int STATE_CLOSED_BEFORE_CONNECTION = 3;
stateToString(int state)249         private static String stateToString(int state) {
250             switch (state) {
251                 case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION";
252                 case STATE_CONNECTED: return "STATE_CONNECTED";
253                 case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION";
254                 case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION";
255                 default: return "Unexpected state=" + state;
256             }
257         }
258         private int mState = STATE_WAIT_CONNECTION;
259 
260         private ISpellCheckerSession mISpellCheckerSession;
261         private HandlerThread mThread;
262         private Handler mAsyncHandler;
263 
SpellCheckerSessionListenerImpl(Handler handler)264         public SpellCheckerSessionListenerImpl(Handler handler) {
265             mHandler = handler;
266         }
267 
268         private static class SpellCheckerParams {
269             public final int mWhat;
270             public final TextInfo[] mTextInfos;
271             public final int mSuggestionsLimit;
272             public final boolean mSequentialWords;
273             public ISpellCheckerSession mSession;
SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)274             public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
275                     boolean sequentialWords) {
276                 mWhat = what;
277                 mTextInfos = textInfos;
278                 mSuggestionsLimit = suggestionsLimit;
279                 mSequentialWords = sequentialWords;
280             }
281         }
282 
processTask(ISpellCheckerSession session, SpellCheckerParams scp, boolean async)283         private void processTask(ISpellCheckerSession session, SpellCheckerParams scp,
284                 boolean async) {
285             if (DBG) {
286                 synchronized (this) {
287                     Log.d(TAG, "entering processTask:"
288                             + " session.hashCode()=#" + Integer.toHexString(session.hashCode())
289                             + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async
290                             + " mAsyncHandler=" + mAsyncHandler
291                             + " mState=" + stateToString(mState));
292                 }
293             }
294             if (async || mAsyncHandler == null) {
295                 switch (scp.mWhat) {
296                     case TASK_CANCEL:
297                         try {
298                             session.onCancel();
299                         } catch (RemoteException e) {
300                             Log.e(TAG, "Failed to cancel " + e);
301                         }
302                         break;
303                     case TASK_GET_SUGGESTIONS_MULTIPLE:
304                         try {
305                             session.onGetSuggestionsMultiple(scp.mTextInfos,
306                                     scp.mSuggestionsLimit, scp.mSequentialWords);
307                         } catch (RemoteException e) {
308                             Log.e(TAG, "Failed to get suggestions " + e);
309                         }
310                         break;
311                     case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
312                         try {
313                             session.onGetSentenceSuggestionsMultiple(
314                                     scp.mTextInfos, scp.mSuggestionsLimit);
315                         } catch (RemoteException e) {
316                             Log.e(TAG, "Failed to get suggestions " + e);
317                         }
318                         break;
319                     case TASK_CLOSE:
320                         try {
321                             session.onClose();
322                         } catch (RemoteException e) {
323                             Log.e(TAG, "Failed to close " + e);
324                         }
325                         break;
326                 }
327             } else {
328                 // The interface is to a local object, so need to execute it
329                 // asynchronously.
330                 scp.mSession = session;
331                 mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp));
332             }
333 
334             if (scp.mWhat == TASK_CLOSE) {
335                 // If we are closing, we want to clean up our state now even
336                 // if it is pending as an async operation.
337                 synchronized (this) {
338                     processCloseLocked();
339                 }
340             }
341         }
342 
processCloseLocked()343         private void processCloseLocked() {
344             if (DBG) Log.d(TAG, "entering processCloseLocked:"
345                     + " session" + (mISpellCheckerSession != null ? ".hashCode()=#"
346                             + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null")
347                     + " mState=" + stateToString(mState));
348             mISpellCheckerSession = null;
349             if (mThread != null) {
350                 mThread.quit();
351             }
352             mHandler = null;
353             mPendingTasks.clear();
354             mThread = null;
355             mAsyncHandler = null;
356             switch (mState) {
357                 case STATE_WAIT_CONNECTION:
358                     mState = STATE_CLOSED_BEFORE_CONNECTION;
359                     break;
360                 case STATE_CONNECTED:
361                     mState = STATE_CLOSED_AFTER_CONNECTION;
362                     break;
363                 default:
364                     Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" +
365                             stateToString(mState));
366                     break;
367             }
368         }
369 
onServiceConnected(ISpellCheckerSession session)370         public void onServiceConnected(ISpellCheckerSession session) {
371             synchronized (this) {
372                 switch (mState) {
373                     case STATE_WAIT_CONNECTION:
374                         // OK, go ahead.
375                         break;
376                     case STATE_CLOSED_BEFORE_CONNECTION:
377                         // This is possible, and not an error.  The client no longer is interested
378                         // in this connection. OK to ignore.
379                         if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is"
380                                 + " already closed.");
381                         return;
382                     default:
383                         Log.e(TAG, "ignoring onServiceConnected due to unexpected mState="
384                                 + stateToString(mState));
385                         return;
386                 }
387                 if (session == null) {
388                     Log.e(TAG, "ignoring onServiceConnected due to session=null");
389                     return;
390                 }
391                 mISpellCheckerSession = session;
392                 if (session.asBinder() instanceof Binder && mThread == null) {
393                     if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected.");
394                     // If this is a local object, we need to do our own threading
395                     // to make sure we handle it asynchronously.
396                     mThread = new HandlerThread("SpellCheckerSession",
397                             Process.THREAD_PRIORITY_BACKGROUND);
398                     mThread.start();
399                     mAsyncHandler = new Handler(mThread.getLooper()) {
400                         @Override public void handleMessage(Message msg) {
401                             SpellCheckerParams scp = (SpellCheckerParams)msg.obj;
402                             processTask(scp.mSession, scp, true);
403                         }
404                     };
405                 }
406                 mState = STATE_CONNECTED;
407                 if (DBG) {
408                     Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#"
409                             + Integer.toHexString(mISpellCheckerSession.hashCode())
410                             + " mPendingTasks.size()=" + mPendingTasks.size());
411                 }
412                 while (!mPendingTasks.isEmpty()) {
413                     processTask(session, mPendingTasks.poll(), false);
414                 }
415             }
416         }
417 
cancel()418         public void cancel() {
419             processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
420         }
421 
getSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)422         public void getSuggestionsMultiple(
423                 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
424             processOrEnqueueTask(
425                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
426                             suggestionsLimit, sequentialWords));
427         }
428 
getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)429         public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
430             processOrEnqueueTask(
431                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
432                             textInfos, suggestionsLimit, false));
433         }
434 
close()435         public void close() {
436             processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
437         }
438 
isDisconnected()439         public boolean isDisconnected() {
440             synchronized (this) {
441                 return mState != STATE_CONNECTED;
442             }
443         }
444 
processOrEnqueueTask(SpellCheckerParams scp)445         private void processOrEnqueueTask(SpellCheckerParams scp) {
446             ISpellCheckerSession session;
447             synchronized (this) {
448                 if (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION
449                         || mState == STATE_CLOSED_BEFORE_CONNECTION)) {
450                     // It is OK to call SpellCheckerSession#close() multiple times.
451                     // Don't output confusing/misleading warning messages.
452                     return;
453                 }
454                 if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) {
455                     Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState="
456                             + stateToString(mState)
457                             + " scp.mWhat=" + taskToString(scp.mWhat));
458                     return;
459                 }
460 
461                 if (mState == STATE_WAIT_CONNECTION) {
462                     // If we are still waiting for the connection. Need to pay special attention.
463                     if (scp.mWhat == TASK_CLOSE) {
464                         processCloseLocked();
465                         return;
466                     }
467                     // Enqueue the task to task queue.
468                     SpellCheckerParams closeTask = null;
469                     if (scp.mWhat == TASK_CANCEL) {
470                         if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask.");
471                         while (!mPendingTasks.isEmpty()) {
472                             final SpellCheckerParams tmp = mPendingTasks.poll();
473                             if (tmp.mWhat == TASK_CLOSE) {
474                                 // Only one close task should be processed, while we need to remove
475                                 // all close tasks from the queue
476                                 closeTask = tmp;
477                             }
478                         }
479                     }
480                     mPendingTasks.offer(scp);
481                     if (closeTask != null) {
482                         mPendingTasks.offer(closeTask);
483                     }
484                     if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the"
485                             + " connection is not established."
486                             + " mPendingTasks.size()=" + mPendingTasks.size());
487                     return;
488                 }
489 
490                 session = mISpellCheckerSession;
491             }
492             // session must never be null here.
493             processTask(session, scp, false);
494         }
495 
496         @Override
onGetSuggestions(SuggestionsInfo[] results)497         public void onGetSuggestions(SuggestionsInfo[] results) {
498             synchronized (this) {
499                 if (mHandler != null) {
500                     mHandler.sendMessage(Message.obtain(mHandler,
501                             MSG_ON_GET_SUGGESTION_MULTIPLE, results));
502                 }
503             }
504         }
505 
506         @Override
onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)507         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
508             synchronized (this) {
509                 if (mHandler != null) {
510                     mHandler.sendMessage(Message.obtain(mHandler,
511                             MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
512                 }
513             }
514         }
515     }
516 
517     /**
518      * Callback for getting results from text services
519      */
520     public interface SpellCheckerSessionListener {
521         /**
522          * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)}
523          * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
524          * @param results an array of {@link SuggestionsInfo}s.
525          * These results are suggestions for {@link TextInfo}s queried by
526          * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or
527          * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
528          */
onGetSuggestions(SuggestionsInfo[] results)529         public void onGetSuggestions(SuggestionsInfo[] results);
530         /**
531          * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
532          * @param results an array of {@link SentenceSuggestionsInfo}s.
533          * These results are suggestions for {@link TextInfo}s
534          * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
535          */
onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)536         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
537     }
538 
539     private static final class InternalListener extends ITextServicesSessionListener.Stub {
540         private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
541 
InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl)542         public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
543             mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
544         }
545 
546         @Override
onServiceConnected(ISpellCheckerSession session)547         public void onServiceConnected(ISpellCheckerSession session) {
548             mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
549         }
550     }
551 
552     @Override
finalize()553     protected void finalize() throws Throwable {
554         try {
555             // Note that mGuard will be null if the constructor threw.
556             if (mGuard != null) {
557                 mGuard.warnIfOpen();
558                 close();
559             }
560         } finally {
561             super.finalize();
562         }
563     }
564 
565     /**
566      * @hide
567      */
getTextServicesSessionListener()568     public ITextServicesSessionListener getTextServicesSessionListener() {
569         return mInternalListener;
570     }
571 
572     /**
573      * @hide
574      */
getSpellCheckerSessionListener()575     public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
576         return mSpellCheckerSessionListenerImpl;
577     }
578 }
579