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.compat.annotation.UnsupportedAppUsage;
20 import android.os.Binder;
21 import android.os.Handler;
22 import android.os.HandlerThread;
23 import android.os.Message;
24 import android.os.Process;
25 import android.os.RemoteException;
26 import android.util.Log;
27 
28 import com.android.internal.textservice.ISpellCheckerSession;
29 import com.android.internal.textservice.ISpellCheckerSessionListener;
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 TextServicesManager mTextServicesManager;
99     private final SpellCheckerInfo mSpellCheckerInfo;
100     @UnsupportedAppUsage
101     private final SpellCheckerSessionListener mSpellCheckerSessionListener;
102     private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
103 
104     private final CloseGuard mGuard = CloseGuard.get();
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                     handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj);
116                     break;
117             }
118         }
119     };
120 
121     /**
122      * Constructor
123      * @hide
124      */
SpellCheckerSession( SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener)125     public SpellCheckerSession(
126             SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener) {
127         if (info == null || listener == null || tsm == null) {
128             throw new NullPointerException();
129         }
130         mSpellCheckerInfo = info;
131         mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler);
132         mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
133         mTextServicesManager = tsm;
134         mSpellCheckerSessionListener = listener;
135 
136         mGuard.open("finishSession");
137     }
138 
139     /**
140      * @return true if the connection to a text service of this session is disconnected and not
141      * alive.
142      */
isSessionDisconnected()143     public boolean isSessionDisconnected() {
144         return mSpellCheckerSessionListenerImpl.isDisconnected();
145     }
146 
147     /**
148      * Get the spell checker service info this spell checker session has.
149      * @return SpellCheckerInfo for the specified locale.
150      */
getSpellChecker()151     public SpellCheckerInfo getSpellChecker() {
152         return mSpellCheckerInfo;
153     }
154 
155     /**
156      * Cancel pending and running spell check tasks
157      */
cancel()158     public void cancel() {
159         mSpellCheckerSessionListenerImpl.cancel();
160     }
161 
162     /**
163      * Finish this session and allow TextServicesManagerService to disconnect the bound spell
164      * checker.
165      */
close()166     public void close() {
167         mGuard.close();
168         mSpellCheckerSessionListenerImpl.close();
169         mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
170     }
171 
172     /**
173      * Get suggestions from the specified sentences
174      * @param textInfos an array of text metadata for a spell checker
175      * @param suggestionsLimit the maximum number of suggestions that will be returned
176      */
getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit)177     public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) {
178         mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple(
179                 textInfos, suggestionsLimit);
180     }
181 
182     /**
183      * Get candidate strings for a substring of the specified text.
184      * @param textInfo text metadata for a spell checker
185      * @param suggestionsLimit the maximum number of suggestions that will be returned
186      * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
187      */
188     @Deprecated
getSuggestions(TextInfo textInfo, int suggestionsLimit)189     public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
190         getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
191     }
192 
193     /**
194      * A batch process of getSuggestions
195      * @param textInfos an array of text metadata for a spell checker
196      * @param suggestionsLimit the maximum number of suggestions that will be returned
197      * @param sequentialWords true if textInfos can be treated as sequential words.
198      * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
199      */
200     @Deprecated
getSuggestions( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)201     public void getSuggestions(
202             TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
203         if (DBG) {
204             Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
205         }
206         mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
207                 textInfos, suggestionsLimit, sequentialWords);
208     }
209 
handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos)210     private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) {
211         mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
212     }
213 
handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos)214     private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) {
215         mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos);
216     }
217 
218     private static final class SpellCheckerSessionListenerImpl
219             extends ISpellCheckerSessionListener.Stub {
220         private static final int TASK_CANCEL = 1;
221         private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
222         private static final int TASK_CLOSE = 3;
223         private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
taskToString(int task)224         private static String taskToString(int task) {
225             switch (task) {
226                 case TASK_CANCEL:
227                     return "TASK_CANCEL";
228                 case TASK_GET_SUGGESTIONS_MULTIPLE:
229                     return "TASK_GET_SUGGESTIONS_MULTIPLE";
230                 case TASK_CLOSE:
231                     return "TASK_CLOSE";
232                 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
233                     return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE";
234                 default:
235                     return "Unexpected task=" + task;
236             }
237         }
238 
239         private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<>();
240         private Handler mHandler;
241 
242         private static final int STATE_WAIT_CONNECTION = 0;
243         private static final int STATE_CONNECTED = 1;
244         private static final int STATE_CLOSED_AFTER_CONNECTION = 2;
245         private static final int STATE_CLOSED_BEFORE_CONNECTION = 3;
stateToString(int state)246         private static String stateToString(int state) {
247             switch (state) {
248                 case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION";
249                 case STATE_CONNECTED: return "STATE_CONNECTED";
250                 case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION";
251                 case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION";
252                 default: return "Unexpected state=" + state;
253             }
254         }
255         private int mState = STATE_WAIT_CONNECTION;
256 
257         private ISpellCheckerSession mISpellCheckerSession;
258         private HandlerThread mThread;
259         private Handler mAsyncHandler;
260 
SpellCheckerSessionListenerImpl(Handler handler)261         public SpellCheckerSessionListenerImpl(Handler handler) {
262             mHandler = handler;
263         }
264 
265         private static class SpellCheckerParams {
266             public final int mWhat;
267             public final TextInfo[] mTextInfos;
268             public final int mSuggestionsLimit;
269             public final boolean mSequentialWords;
270             public ISpellCheckerSession mSession;
SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)271             public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
272                     boolean sequentialWords) {
273                 mWhat = what;
274                 mTextInfos = textInfos;
275                 mSuggestionsLimit = suggestionsLimit;
276                 mSequentialWords = sequentialWords;
277             }
278         }
279 
processTask(ISpellCheckerSession session, SpellCheckerParams scp, boolean async)280         private void processTask(ISpellCheckerSession session, SpellCheckerParams scp,
281                 boolean async) {
282             if (DBG) {
283                 synchronized (this) {
284                     Log.d(TAG, "entering processTask:"
285                             + " session.hashCode()=#" + Integer.toHexString(session.hashCode())
286                             + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async
287                             + " mAsyncHandler=" + mAsyncHandler
288                             + " mState=" + stateToString(mState));
289                 }
290             }
291             if (async || mAsyncHandler == null) {
292                 switch (scp.mWhat) {
293                     case TASK_CANCEL:
294                         try {
295                             session.onCancel();
296                         } catch (RemoteException e) {
297                             Log.e(TAG, "Failed to cancel " + e);
298                         }
299                         break;
300                     case TASK_GET_SUGGESTIONS_MULTIPLE:
301                         try {
302                             session.onGetSuggestionsMultiple(scp.mTextInfos,
303                                     scp.mSuggestionsLimit, scp.mSequentialWords);
304                         } catch (RemoteException e) {
305                             Log.e(TAG, "Failed to get suggestions " + e);
306                         }
307                         break;
308                     case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
309                         try {
310                             session.onGetSentenceSuggestionsMultiple(
311                                     scp.mTextInfos, scp.mSuggestionsLimit);
312                         } catch (RemoteException e) {
313                             Log.e(TAG, "Failed to get suggestions " + e);
314                         }
315                         break;
316                     case TASK_CLOSE:
317                         try {
318                             session.onClose();
319                         } catch (RemoteException e) {
320                             Log.e(TAG, "Failed to close " + e);
321                         }
322                         break;
323                 }
324             } else {
325                 // The interface is to a local object, so need to execute it
326                 // asynchronously.
327                 scp.mSession = session;
328                 mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp));
329             }
330 
331             if (scp.mWhat == TASK_CLOSE) {
332                 // If we are closing, we want to clean up our state now even
333                 // if it is pending as an async operation.
334                 synchronized (this) {
335                     processCloseLocked();
336                 }
337             }
338         }
339 
processCloseLocked()340         private void processCloseLocked() {
341             if (DBG) Log.d(TAG, "entering processCloseLocked:"
342                     + " session" + (mISpellCheckerSession != null ? ".hashCode()=#"
343                             + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null")
344                     + " mState=" + stateToString(mState));
345             mISpellCheckerSession = null;
346             if (mThread != null) {
347                 mThread.quit();
348             }
349             mHandler = null;
350             mPendingTasks.clear();
351             mThread = null;
352             mAsyncHandler = null;
353             switch (mState) {
354                 case STATE_WAIT_CONNECTION:
355                     mState = STATE_CLOSED_BEFORE_CONNECTION;
356                     break;
357                 case STATE_CONNECTED:
358                     mState = STATE_CLOSED_AFTER_CONNECTION;
359                     break;
360                 default:
361                     Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" +
362                             stateToString(mState));
363                     break;
364             }
365         }
366 
onServiceConnected(ISpellCheckerSession session)367         public void onServiceConnected(ISpellCheckerSession session) {
368             synchronized (this) {
369                 switch (mState) {
370                     case STATE_WAIT_CONNECTION:
371                         // OK, go ahead.
372                         break;
373                     case STATE_CLOSED_BEFORE_CONNECTION:
374                         // This is possible, and not an error.  The client no longer is interested
375                         // in this connection. OK to ignore.
376                         if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is"
377                                 + " already closed.");
378                         return;
379                     default:
380                         Log.e(TAG, "ignoring onServiceConnected due to unexpected mState="
381                                 + stateToString(mState));
382                         return;
383                 }
384                 if (session == null) {
385                     Log.e(TAG, "ignoring onServiceConnected due to session=null");
386                     return;
387                 }
388                 mISpellCheckerSession = session;
389                 if (session.asBinder() instanceof Binder && mThread == null) {
390                     if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected.");
391                     // If this is a local object, we need to do our own threading
392                     // to make sure we handle it asynchronously.
393                     mThread = new HandlerThread("SpellCheckerSession",
394                             Process.THREAD_PRIORITY_BACKGROUND);
395                     mThread.start();
396                     mAsyncHandler = new Handler(mThread.getLooper()) {
397                         @Override public void handleMessage(Message msg) {
398                             SpellCheckerParams scp = (SpellCheckerParams)msg.obj;
399                             processTask(scp.mSession, scp, true);
400                         }
401                     };
402                 }
403                 mState = STATE_CONNECTED;
404                 if (DBG) {
405                     Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#"
406                             + Integer.toHexString(mISpellCheckerSession.hashCode())
407                             + " mPendingTasks.size()=" + mPendingTasks.size());
408                 }
409                 while (!mPendingTasks.isEmpty()) {
410                     processTask(session, mPendingTasks.poll(), false);
411                 }
412             }
413         }
414 
cancel()415         public void cancel() {
416             processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
417         }
418 
getSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)419         public void getSuggestionsMultiple(
420                 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
421             processOrEnqueueTask(
422                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
423                             suggestionsLimit, sequentialWords));
424         }
425 
getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)426         public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
427             processOrEnqueueTask(
428                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
429                             textInfos, suggestionsLimit, false));
430         }
431 
close()432         public void close() {
433             processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
434         }
435 
isDisconnected()436         public boolean isDisconnected() {
437             synchronized (this) {
438                 return mState != STATE_CONNECTED;
439             }
440         }
441 
processOrEnqueueTask(SpellCheckerParams scp)442         private void processOrEnqueueTask(SpellCheckerParams scp) {
443             ISpellCheckerSession session;
444             synchronized (this) {
445                 if (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION
446                         || mState == STATE_CLOSED_BEFORE_CONNECTION)) {
447                     // It is OK to call SpellCheckerSession#close() multiple times.
448                     // Don't output confusing/misleading warning messages.
449                     return;
450                 }
451                 if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) {
452                     Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState="
453                             + stateToString(mState)
454                             + " scp.mWhat=" + taskToString(scp.mWhat));
455                     return;
456                 }
457 
458                 if (mState == STATE_WAIT_CONNECTION) {
459                     // If we are still waiting for the connection. Need to pay special attention.
460                     if (scp.mWhat == TASK_CLOSE) {
461                         processCloseLocked();
462                         return;
463                     }
464                     // Enqueue the task to task queue.
465                     SpellCheckerParams closeTask = null;
466                     if (scp.mWhat == TASK_CANCEL) {
467                         if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask.");
468                         while (!mPendingTasks.isEmpty()) {
469                             final SpellCheckerParams tmp = mPendingTasks.poll();
470                             if (tmp.mWhat == TASK_CLOSE) {
471                                 // Only one close task should be processed, while we need to remove
472                                 // all close tasks from the queue
473                                 closeTask = tmp;
474                             }
475                         }
476                     }
477                     mPendingTasks.offer(scp);
478                     if (closeTask != null) {
479                         mPendingTasks.offer(closeTask);
480                     }
481                     if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the"
482                             + " connection is not established."
483                             + " mPendingTasks.size()=" + mPendingTasks.size());
484                     return;
485                 }
486 
487                 session = mISpellCheckerSession;
488             }
489             // session must never be null here.
490             processTask(session, scp, false);
491         }
492 
493         @Override
onGetSuggestions(SuggestionsInfo[] results)494         public void onGetSuggestions(SuggestionsInfo[] results) {
495             synchronized (this) {
496                 if (mHandler != null) {
497                     mHandler.sendMessage(Message.obtain(mHandler,
498                             MSG_ON_GET_SUGGESTION_MULTIPLE, results));
499                 }
500             }
501         }
502 
503         @Override
onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)504         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
505             synchronized (this) {
506                 if (mHandler != null) {
507                     mHandler.sendMessage(Message.obtain(mHandler,
508                             MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
509                 }
510             }
511         }
512     }
513 
514     /**
515      * Callback for getting results from text services
516      */
517     public interface SpellCheckerSessionListener {
518         /**
519          * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)}
520          * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
521          * @param results an array of {@link SuggestionsInfo}s.
522          * These results are suggestions for {@link TextInfo}s queried by
523          * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or
524          * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
525          */
onGetSuggestions(SuggestionsInfo[] results)526         public void onGetSuggestions(SuggestionsInfo[] results);
527         /**
528          * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
529          * @param results an array of {@link SentenceSuggestionsInfo}s.
530          * These results are suggestions for {@link TextInfo}s
531          * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
532          */
onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)533         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
534     }
535 
536     private static final class InternalListener extends ITextServicesSessionListener.Stub {
537         private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
538 
InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl)539         public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
540             mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
541         }
542 
543         @Override
onServiceConnected(ISpellCheckerSession session)544         public void onServiceConnected(ISpellCheckerSession session) {
545             mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
546         }
547     }
548 
549     @Override
finalize()550     protected void finalize() throws Throwable {
551         try {
552             // Note that mGuard will be null if the constructor threw.
553             if (mGuard != null) {
554                 mGuard.warnIfOpen();
555                 close();
556             }
557         } finally {
558             super.finalize();
559         }
560     }
561 
562     /**
563      * @hide
564      */
getTextServicesSessionListener()565     public ITextServicesSessionListener getTextServicesSessionListener() {
566         return mInternalListener;
567     }
568 
569     /**
570      * @hide
571      */
getSpellCheckerSessionListener()572     public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
573         return mSpellCheckerSessionListenerImpl;
574     }
575 }
576