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