SpellCheckerSession.java revision b5052de75736527549d7e537632777c6fec2e4f0
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view.textservice;
18
19import com.android.internal.textservice.ISpellCheckerSession;
20import com.android.internal.textservice.ISpellCheckerSessionListener;
21import com.android.internal.textservice.ITextServicesManager;
22import com.android.internal.textservice.ITextServicesSessionListener;
23
24import android.os.Binder;
25import android.os.Handler;
26import android.os.HandlerThread;
27import android.os.Message;
28import android.os.Process;
29import android.os.RemoteException;
30import android.util.Log;
31import android.view.textservice.SpellCheckerInfo;
32import android.view.textservice.SuggestionsInfo;
33import android.view.textservice.TextInfo;
34
35import java.util.LinkedList;
36import 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 */
86public 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    private static final String SUPPORT_SENTENCE_SPELL_CHECK = "SupportSentenceSpellCheck";
95
96
97    private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
98    private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
99
100    private final InternalListener mInternalListener;
101    private final ITextServicesManager mTextServicesManager;
102    private final SpellCheckerInfo mSpellCheckerInfo;
103    private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
104    private final SpellCheckerSubtype mSubtype;
105
106    private boolean mIsUsed;
107    private SpellCheckerSessionListener mSpellCheckerSessionListener;
108
109    /** Handler that will execute the main tasks */
110    private final Handler mHandler = new Handler() {
111        @Override
112        public void handleMessage(Message msg) {
113            switch (msg.what) {
114                case MSG_ON_GET_SUGGESTION_MULTIPLE:
115                    handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj);
116                    break;
117                case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE:
118                    handleOnGetSuggestionsMultipleForSentence((SuggestionsInfo[]) msg.obj);
119                    break;
120            }
121        }
122    };
123
124    /**
125     * Constructor
126     * @hide
127     */
128    public SpellCheckerSession(
129            SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener,
130            SpellCheckerSubtype subtype) {
131        if (info == null || listener == null || tsm == null) {
132            throw new NullPointerException();
133        }
134        mSpellCheckerInfo = info;
135        mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler);
136        mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
137        mTextServicesManager = tsm;
138        mIsUsed = true;
139        mSpellCheckerSessionListener = listener;
140        mSubtype = subtype;
141    }
142
143    /**
144     * @return true if the connection to a text service of this session is disconnected and not
145     * alive.
146     */
147    public boolean isSessionDisconnected() {
148        return mSpellCheckerSessionListenerImpl.isDisconnected();
149    }
150
151    /**
152     * Get the spell checker service info this spell checker session has.
153     * @return SpellCheckerInfo for the specified locale.
154     */
155    public SpellCheckerInfo getSpellChecker() {
156        return mSpellCheckerInfo;
157    }
158
159    /**
160     * Cancel pending and running spell check tasks
161     */
162    public void cancel() {
163        mSpellCheckerSessionListenerImpl.cancel();
164    }
165
166    /**
167     * Finish this session and allow TextServicesManagerService to disconnect the bound spell
168     * checker.
169     */
170    public void close() {
171        mIsUsed = false;
172        try {
173            mSpellCheckerSessionListenerImpl.close();
174            mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
175        } catch (RemoteException e) {
176            // do nothing
177        }
178    }
179
180    /**
181     * @hide
182     */
183    public void getSuggestionsForSentence(TextInfo textInfo, int suggestionsLimit) {
184        mSpellCheckerSessionListenerImpl.getSuggestionsMultipleForSentence(
185                new TextInfo[] {textInfo}, 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 number of limit of suggestions returned
192     */
193    public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
194        getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
195    }
196
197    /**
198     * A batch process of getSuggestions
199     * @param textInfos an array of text metadata for a spell checker
200     * @param suggestionsLimit the number of limit of suggestions returned
201     * @param sequentialWords true if textInfos can be treated as sequential words.
202     */
203    public void getSuggestions(
204            TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
205        if (DBG) {
206            Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
207        }
208        // TODO: Handle multiple words suggestions by using WordBreakIterator
209        mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
210                textInfos, suggestionsLimit, sequentialWords);
211    }
212
213    private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) {
214        mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
215    }
216
217    private void handleOnGetSuggestionsMultipleForSentence(SuggestionsInfo[] suggestionInfos) {
218        mSpellCheckerSessionListener.onGetSuggestionsForSentence(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;
226        private final Queue<SpellCheckerParams> mPendingTasks =
227                new LinkedList<SpellCheckerParams>();
228        private Handler mHandler;
229
230        private boolean mOpened;
231        private ISpellCheckerSession mISpellCheckerSession;
232        private HandlerThread mThread;
233        private Handler mAsyncHandler;
234
235        public SpellCheckerSessionListenerImpl(Handler handler) {
236            mOpened = false;
237            mHandler = handler;
238        }
239
240        private static class SpellCheckerParams {
241            public final int mWhat;
242            public final TextInfo[] mTextInfos;
243            public final int mSuggestionsLimit;
244            public final boolean mSequentialWords;
245            public ISpellCheckerSession mSession;
246            public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
247                    boolean sequentialWords) {
248                mWhat = what;
249                mTextInfos = textInfos;
250                mSuggestionsLimit = suggestionsLimit;
251                mSequentialWords = sequentialWords;
252            }
253        }
254
255        private void processTask(ISpellCheckerSession session, SpellCheckerParams scp,
256                boolean async) {
257            if (async || mAsyncHandler == null) {
258                switch (scp.mWhat) {
259                    case TASK_CANCEL:
260                        if (DBG) {
261                            Log.w(TAG, "Cancel spell checker tasks.");
262                        }
263                        try {
264                            session.onCancel();
265                        } catch (RemoteException e) {
266                            Log.e(TAG, "Failed to cancel " + e);
267                        }
268                        break;
269                    case TASK_GET_SUGGESTIONS_MULTIPLE:
270                        if (DBG) {
271                            Log.w(TAG, "Get suggestions from the spell checker.");
272                        }
273                        try {
274                            session.onGetSuggestionsMultiple(scp.mTextInfos,
275                                    scp.mSuggestionsLimit, scp.mSequentialWords);
276                        } catch (RemoteException e) {
277                            Log.e(TAG, "Failed to get suggestions " + e);
278                        }
279                        break;
280                    case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
281                        if (DBG) {
282                            Log.w(TAG, "Get suggestions from the spell checker.");
283                        }
284                        if (scp.mTextInfos.length != 1) {
285                            throw new IllegalArgumentException();
286                        }
287                        try {
288                            session.onGetSuggestionsMultipleForSentence(
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
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
352        public void cancel() {
353            if (DBG) {
354                Log.w(TAG, "cancel");
355            }
356            processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
357        }
358
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
369        public void getSuggestionsMultipleForSentence(TextInfo[] textInfos, int suggestionsLimit) {
370            if (DBG) {
371                Log.w(TAG, "getSuggestionsMultipleForSentence");
372            }
373            processOrEnqueueTask(
374                    new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
375                            textInfos, suggestionsLimit, false));
376        }
377
378        public void close() {
379            if (DBG) {
380                Log.w(TAG, "close");
381            }
382            processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
383        }
384
385        public boolean isDisconnected() {
386            return mOpened && mISpellCheckerSession == null;
387        }
388
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 all
403                                // 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
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
429        public void onGetSuggestionsForSentence(SuggestionsInfo[] 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 "getSuggestions"
441         * @param results an array of results of getSuggestions
442         */
443        public void onGetSuggestions(SuggestionsInfo[] results);
444        /**
445         * @hide
446         */
447        public void onGetSuggestionsForSentence(SuggestionsInfo[] results);
448    }
449
450    private static class InternalListener extends ITextServicesSessionListener.Stub {
451        private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
452
453        public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
454            mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
455        }
456
457        @Override
458        public void onServiceConnected(ISpellCheckerSession session) {
459            if (DBG) {
460                Log.w(TAG, "SpellCheckerSession connected.");
461            }
462            mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
463        }
464    }
465
466    @Override
467    protected void finalize() throws Throwable {
468        super.finalize();
469        if (mIsUsed) {
470            Log.e(TAG, "SpellCheckerSession was not finished properly." +
471                    "You should call finishShession() when you finished to use a spell checker.");
472            close();
473        }
474    }
475
476    /**
477     * @hide
478     */
479    public ITextServicesSessionListener getTextServicesSessionListener() {
480        return mInternalListener;
481    }
482
483    /**
484     * @hide
485     */
486    public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
487        return mSpellCheckerSessionListenerImpl;
488    }
489
490    /**
491     * @hide
492     */
493    public boolean isSentenceSpellCheckSupported() {
494        return mSubtype.containsExtraValueKey(SUPPORT_SENTENCE_SPELL_CHECK);
495    }
496}
497