• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.view.textservice;
18 
19 import com.android.internal.textservice.ISpellCheckerSession;
20 import com.android.internal.textservice.ISpellCheckerSessionListener;
21 import com.android.internal.textservice.ITextServicesManager;
22 import com.android.internal.textservice.ITextServicesSessionListener;
23 
24 import android.os.Binder;
25 import android.os.Handler;
26 import android.os.HandlerThread;
27 import android.os.Message;
28 import android.os.Process;
29 import android.os.RemoteException;
30 import android.util.Log;
31 import android.view.textservice.SpellCheckerInfo;
32 import android.view.textservice.SuggestionsInfo;
33 import android.view.textservice.TextInfo;
34 
35 import java.util.LinkedList;
36 import java.util.Queue;
37 
38 /**
39  * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService.
40  *
41  *
42  * <a name="Applications"></a>
43  * <h3>Applications</h3>
44  *
45  * <p>In most cases, applications that are using the standard
46  * {@link android.widget.TextView} or its subclasses will have little they need
47  * to do to work well with spell checker services.  The main things you need to
48  * be aware of are:</p>
49  *
50  * <ul>
51  * <li> Properly set the {@link android.R.attr#inputType} in your editable
52  * text views, so that the spell checker will have enough context to help the
53  * user in editing text in them.
54  * </ul>
55  *
56  * <p>For the rare people amongst us writing client applications that use the spell checker service
57  * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or
58  * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker
59  * service by yourself.</p>
60  *
61  * <h3>Security</h3>
62  *
63  * <p>There are a lot of security issues associated with spell checkers,
64  * since they could monitor all the text being sent to them
65  * through, for instance, {@link android.widget.TextView}.
66  * The Android spell checker framework also allows
67  * arbitrary third party spell checkers, so care must be taken to restrict their
68  * selection and interactions.</p>
69  *
70  * <p>Here are some key points about the security architecture behind the
71  * spell checker framework:</p>
72  *
73  * <ul>
74  * <li>Only the system is allowed to directly access a spell checker framework's
75  * {@link android.service.textservice.SpellCheckerService} interface, via the
76  * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission.  This is
77  * enforced in the system by not binding to a spell checker service that does
78  * not require this permission.
79  *
80  * <li>The user must explicitly enable a new spell checker in settings before
81  * they can be enabled, to confirm with the system that they know about it
82  * and want to make it available for use.
83  * </ul>
84  *
85  */
86 public class SpellCheckerSession {
87     private static final String TAG = SpellCheckerSession.class.getSimpleName();
88     private static final boolean DBG = false;
89     /**
90      * Name under which a SpellChecker service component publishes information about itself.
91      * This meta-data must reference an XML resource.
92      **/
93     public static final String SERVICE_META_DATA = "android.view.textservice.scs";
94 
95     private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
96     private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
97 
98     private final InternalListener mInternalListener;
99     private final ITextServicesManager mTextServicesManager;
100     private final SpellCheckerInfo mSpellCheckerInfo;
101     private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
102     private final SpellCheckerSubtype mSubtype;
103 
104     private boolean mIsUsed;
105     private SpellCheckerSessionListener mSpellCheckerSessionListener;
106 
107     /** Handler that will execute the main tasks */
108     private final Handler mHandler = new Handler() {
109         @Override
110         public void handleMessage(Message msg) {
111             switch (msg.what) {
112                 case MSG_ON_GET_SUGGESTION_MULTIPLE:
113                     handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj);
114                     break;
115                 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE:
116                     handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj);
117                     break;
118             }
119         }
120     };
121 
122     /**
123      * Constructor
124      * @hide
125      */
SpellCheckerSession( SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener, SpellCheckerSubtype subtype)126     public SpellCheckerSession(
127             SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener,
128             SpellCheckerSubtype subtype) {
129         if (info == null || listener == null || tsm == null) {
130             throw new NullPointerException();
131         }
132         mSpellCheckerInfo = info;
133         mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler);
134         mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
135         mTextServicesManager = tsm;
136         mIsUsed = true;
137         mSpellCheckerSessionListener = listener;
138         mSubtype = subtype;
139     }
140 
141     /**
142      * @return true if the connection to a text service of this session is disconnected and not
143      * alive.
144      */
isSessionDisconnected()145     public boolean isSessionDisconnected() {
146         return mSpellCheckerSessionListenerImpl.isDisconnected();
147     }
148 
149     /**
150      * Get the spell checker service info this spell checker session has.
151      * @return SpellCheckerInfo for the specified locale.
152      */
getSpellChecker()153     public SpellCheckerInfo getSpellChecker() {
154         return mSpellCheckerInfo;
155     }
156 
157     /**
158      * Cancel pending and running spell check tasks
159      */
cancel()160     public void cancel() {
161         mSpellCheckerSessionListenerImpl.cancel();
162     }
163 
164     /**
165      * Finish this session and allow TextServicesManagerService to disconnect the bound spell
166      * checker.
167      */
close()168     public void close() {
169         mIsUsed = false;
170         try {
171             mSpellCheckerSessionListenerImpl.close();
172             mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
173         } catch (RemoteException e) {
174             // do nothing
175         }
176     }
177 
178     /**
179      * Get suggestions from the specified sentences
180      * @param textInfos an array of text metadata for a spell checker
181      * @param suggestionsLimit the maximum number of suggestions that will be returned
182      */
getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit)183     public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) {
184         mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple(
185                 textInfos, suggestionsLimit);
186     }
187 
188     /**
189      * Get candidate strings for a substring of the specified text.
190      * @param textInfo text metadata for a spell checker
191      * @param suggestionsLimit the maximum number of suggestions that will be returned
192      * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
193      */
194     @Deprecated
getSuggestions(TextInfo textInfo, int suggestionsLimit)195     public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
196         getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
197     }
198 
199     /**
200      * A batch process of getSuggestions
201      * @param textInfos an array of text metadata for a spell checker
202      * @param suggestionsLimit the maximum number of suggestions that will be returned
203      * @param sequentialWords true if textInfos can be treated as sequential words.
204      * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
205      */
206     @Deprecated
getSuggestions( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)207     public void getSuggestions(
208             TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
209         if (DBG) {
210             Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
211         }
212         mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
213                 textInfos, suggestionsLimit, sequentialWords);
214     }
215 
handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos)216     private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) {
217         mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
218     }
219 
handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos)220     private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) {
221         mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos);
222     }
223 
224     private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub {
225         private static final int TASK_CANCEL = 1;
226         private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
227         private static final int TASK_CLOSE = 3;
228         private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
229         private final Queue<SpellCheckerParams> mPendingTasks =
230                 new LinkedList<SpellCheckerParams>();
231         private Handler mHandler;
232 
233         private boolean mOpened;
234         private ISpellCheckerSession mISpellCheckerSession;
235         private HandlerThread mThread;
236         private Handler mAsyncHandler;
237 
SpellCheckerSessionListenerImpl(Handler handler)238         public SpellCheckerSessionListenerImpl(Handler handler) {
239             mOpened = false;
240             mHandler = handler;
241         }
242 
243         private static class SpellCheckerParams {
244             public final int mWhat;
245             public final TextInfo[] mTextInfos;
246             public final int mSuggestionsLimit;
247             public final boolean mSequentialWords;
248             public ISpellCheckerSession mSession;
SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)249             public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
250                     boolean sequentialWords) {
251                 mWhat = what;
252                 mTextInfos = textInfos;
253                 mSuggestionsLimit = suggestionsLimit;
254                 mSequentialWords = sequentialWords;
255             }
256         }
257 
processTask(ISpellCheckerSession session, SpellCheckerParams scp, boolean async)258         private void processTask(ISpellCheckerSession session, SpellCheckerParams scp,
259                 boolean async) {
260             if (async || mAsyncHandler == null) {
261                 switch (scp.mWhat) {
262                     case TASK_CANCEL:
263                         if (DBG) {
264                             Log.w(TAG, "Cancel spell checker tasks.");
265                         }
266                         try {
267                             session.onCancel();
268                         } catch (RemoteException e) {
269                             Log.e(TAG, "Failed to cancel " + e);
270                         }
271                         break;
272                     case TASK_GET_SUGGESTIONS_MULTIPLE:
273                         if (DBG) {
274                             Log.w(TAG, "Get suggestions from the spell checker.");
275                         }
276                         try {
277                             session.onGetSuggestionsMultiple(scp.mTextInfos,
278                                     scp.mSuggestionsLimit, scp.mSequentialWords);
279                         } catch (RemoteException e) {
280                             Log.e(TAG, "Failed to get suggestions " + e);
281                         }
282                         break;
283                     case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
284                         if (DBG) {
285                             Log.w(TAG, "Get sentence suggestions from the spell checker.");
286                         }
287                         try {
288                             session.onGetSentenceSuggestionsMultiple(
289                                     scp.mTextInfos, scp.mSuggestionsLimit);
290                         } catch (RemoteException e) {
291                             Log.e(TAG, "Failed to get suggestions " + e);
292                         }
293                         break;
294                     case TASK_CLOSE:
295                         if (DBG) {
296                             Log.w(TAG, "Close spell checker tasks.");
297                         }
298                         try {
299                             session.onClose();
300                         } catch (RemoteException e) {
301                             Log.e(TAG, "Failed to close " + e);
302                         }
303                         break;
304                 }
305             } else {
306                 // The interface is to a local object, so need to execute it
307                 // asynchronously.
308                 scp.mSession = session;
309                 mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp));
310             }
311 
312             if (scp.mWhat == TASK_CLOSE) {
313                 // If we are closing, we want to clean up our state now even
314                 // if it is pending as an async operation.
315                 synchronized (this) {
316                     mISpellCheckerSession = null;
317                     mHandler = null;
318                     if (mThread != null) {
319                         mThread.quit();
320                     }
321                     mThread = null;
322                     mAsyncHandler = null;
323                 }
324             }
325         }
326 
onServiceConnected(ISpellCheckerSession session)327         public synchronized void onServiceConnected(ISpellCheckerSession session) {
328             synchronized (this) {
329                 mISpellCheckerSession = session;
330                 if (session.asBinder() instanceof Binder && mThread == null) {
331                     // If this is a local object, we need to do our own threading
332                     // to make sure we handle it asynchronously.
333                     mThread = new HandlerThread("SpellCheckerSession",
334                             Process.THREAD_PRIORITY_BACKGROUND);
335                     mThread.start();
336                     mAsyncHandler = new Handler(mThread.getLooper()) {
337                         @Override public void handleMessage(Message msg) {
338                             SpellCheckerParams scp = (SpellCheckerParams)msg.obj;
339                             processTask(scp.mSession, scp, true);
340                         }
341                     };
342                 }
343                 mOpened = true;
344             }
345             if (DBG)
346                 Log.d(TAG, "onServiceConnected - Success");
347             while (!mPendingTasks.isEmpty()) {
348                 processTask(session, mPendingTasks.poll(), false);
349             }
350         }
351 
cancel()352         public void cancel() {
353             if (DBG) {
354                 Log.w(TAG, "cancel");
355             }
356             processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
357         }
358 
getSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)359         public void getSuggestionsMultiple(
360                 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
361             if (DBG) {
362                 Log.w(TAG, "getSuggestionsMultiple");
363             }
364             processOrEnqueueTask(
365                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
366                             suggestionsLimit, sequentialWords));
367         }
368 
getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)369         public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
370             if (DBG) {
371                 Log.w(TAG, "getSentenceSuggestionsMultiple");
372             }
373             processOrEnqueueTask(
374                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
375                             textInfos, suggestionsLimit, false));
376         }
377 
close()378         public void close() {
379             if (DBG) {
380                 Log.w(TAG, "close");
381             }
382             processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
383         }
384 
isDisconnected()385         public boolean isDisconnected() {
386             return mOpened && mISpellCheckerSession == null;
387         }
388 
processOrEnqueueTask(SpellCheckerParams scp)389         private void processOrEnqueueTask(SpellCheckerParams scp) {
390             if (DBG) {
391                 Log.d(TAG, "process or enqueue task: " + mISpellCheckerSession);
392             }
393             ISpellCheckerSession session;
394             synchronized (this) {
395                 session = mISpellCheckerSession;
396                 if (session == null) {
397                     SpellCheckerParams closeTask = null;
398                     if (scp.mWhat == TASK_CANCEL) {
399                         while (!mPendingTasks.isEmpty()) {
400                             final SpellCheckerParams tmp = mPendingTasks.poll();
401                             if (tmp.mWhat == TASK_CLOSE) {
402                                 // Only one close task should be processed, while we need to remove
403                                 // all close tasks from the queue
404                                 closeTask = tmp;
405                             }
406                         }
407                     }
408                     mPendingTasks.offer(scp);
409                     if (closeTask != null) {
410                         mPendingTasks.offer(closeTask);
411                     }
412                     return;
413                 }
414             }
415             processTask(session, scp, false);
416         }
417 
418         @Override
onGetSuggestions(SuggestionsInfo[] results)419         public void onGetSuggestions(SuggestionsInfo[] results) {
420             synchronized (this) {
421                 if (mHandler != null) {
422                     mHandler.sendMessage(Message.obtain(mHandler,
423                             MSG_ON_GET_SUGGESTION_MULTIPLE, results));
424                 }
425             }
426         }
427 
428         @Override
onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)429         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
430             mHandler.sendMessage(
431                     Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
432         }
433     }
434 
435     /**
436      * Callback for getting results from text services
437      */
438     public interface SpellCheckerSessionListener {
439         /**
440          * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)}
441          * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
442          * @param results an array of {@link SuggestionsInfo}s.
443          * These results are suggestions for {@link TextInfo}s queried by
444          * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or
445          * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
446          */
onGetSuggestions(SuggestionsInfo[] results)447         public void onGetSuggestions(SuggestionsInfo[] results);
448         /**
449          * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
450          * @param results an array of {@link SentenceSuggestionsInfo}s.
451          * These results are suggestions for {@link TextInfo}s
452          * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
453          */
onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)454         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
455     }
456 
457     private static class InternalListener extends ITextServicesSessionListener.Stub {
458         private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
459 
InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl)460         public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
461             mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
462         }
463 
464         @Override
onServiceConnected(ISpellCheckerSession session)465         public void onServiceConnected(ISpellCheckerSession session) {
466             if (DBG) {
467                 Log.w(TAG, "SpellCheckerSession connected.");
468             }
469             mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
470         }
471     }
472 
473     @Override
finalize()474     protected void finalize() throws Throwable {
475         super.finalize();
476         if (mIsUsed) {
477             Log.e(TAG, "SpellCheckerSession was not finished properly." +
478                     "You should call finishShession() when you finished to use a spell checker.");
479             close();
480         }
481     }
482 
483     /**
484      * @hide
485      */
getTextServicesSessionListener()486     public ITextServicesSessionListener getTextServicesSessionListener() {
487         return mInternalListener;
488     }
489 
490     /**
491      * @hide
492      */
getSpellCheckerSessionListener()493     public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
494         return mSpellCheckerSessionListenerImpl;
495     }
496 }
497