SpellCheckerSession.java revision 0dc1f648a09b46c45190ba1ce7daecf7fada4347
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.Handler;
25import android.os.Message;
26import android.os.RemoteException;
27import android.util.Log;
28import android.view.textservice.SpellCheckerInfo;
29import android.view.textservice.SuggestionsInfo;
30import android.view.textservice.TextInfo;
31
32import java.util.LinkedList;
33import 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 */
83public 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    private static final String SUPPORT_SENTENCE_SPELL_CHECK = "SupportSentenceSpellCheck";
92
93
94    private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
95    private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
96
97    private final InternalListener mInternalListener;
98    private final ITextServicesManager mTextServicesManager;
99    private final SpellCheckerInfo mSpellCheckerInfo;
100    private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
101    private final SpellCheckerSubtype mSubtype;
102
103    private boolean mIsUsed;
104    private SpellCheckerSessionListener mSpellCheckerSessionListener;
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                    handleOnGetSuggestionsMultipleForSentence((SuggestionsInfo[]) msg.obj);
116                    break;
117            }
118        }
119    };
120
121    /**
122     * Constructor
123     * @hide
124     */
125    public SpellCheckerSession(
126            SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener,
127            SpellCheckerSubtype subtype) {
128        if (info == null || listener == null || tsm == null) {
129            throw new NullPointerException();
130        }
131        mSpellCheckerInfo = info;
132        mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler);
133        mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
134        mTextServicesManager = tsm;
135        mIsUsed = true;
136        mSpellCheckerSessionListener = listener;
137        mSubtype = subtype;
138    }
139
140    /**
141     * @return true if the connection to a text service of this session is disconnected and not
142     * alive.
143     */
144    public boolean isSessionDisconnected() {
145        return mSpellCheckerSessionListenerImpl.isDisconnected();
146    }
147
148    /**
149     * Get the spell checker service info this spell checker session has.
150     * @return SpellCheckerInfo for the specified locale.
151     */
152    public SpellCheckerInfo getSpellChecker() {
153        return mSpellCheckerInfo;
154    }
155
156    /**
157     * Cancel pending and running spell check tasks
158     */
159    public void cancel() {
160        mSpellCheckerSessionListenerImpl.cancel();
161    }
162
163    /**
164     * Finish this session and allow TextServicesManagerService to disconnect the bound spell
165     * checker.
166     */
167    public void close() {
168        mIsUsed = false;
169        try {
170            mSpellCheckerSessionListenerImpl.close();
171            mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
172        } catch (RemoteException e) {
173            // do nothing
174        }
175    }
176
177    /**
178     * @hide
179     */
180    public void getSuggestionsForSentence(TextInfo textInfo, int suggestionsLimit) {
181        mSpellCheckerSessionListenerImpl.getSuggestionsMultipleForSentence(
182                new TextInfo[] {textInfo}, 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 number of limit of suggestions returned
189     */
190    public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
191        getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
192    }
193
194    /**
195     * A batch process of getSuggestions
196     * @param textInfos an array of text metadata for a spell checker
197     * @param suggestionsLimit the number of limit of suggestions returned
198     * @param sequentialWords true if textInfos can be treated as sequential words.
199     */
200    public void getSuggestions(
201            TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
202        if (DBG) {
203            Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
204        }
205        // TODO: Handle multiple words suggestions by using WordBreakIterator
206        mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
207                textInfos, suggestionsLimit, sequentialWords);
208    }
209
210    private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) {
211        mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
212    }
213
214    private void handleOnGetSuggestionsMultipleForSentence(SuggestionsInfo[] suggestionInfos) {
215        mSpellCheckerSessionListener.onGetSuggestionsForSentence(suggestionInfos);
216    }
217
218    private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub {
219        private static final int TASK_CANCEL = 1;
220        private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
221        private static final int TASK_CLOSE = 3;
222        private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
223        private final Queue<SpellCheckerParams> mPendingTasks =
224                new LinkedList<SpellCheckerParams>();
225        private Handler mHandler;
226
227        private boolean mOpened;
228        private ISpellCheckerSession mISpellCheckerSession;
229
230        public SpellCheckerSessionListenerImpl(Handler handler) {
231            mOpened = false;
232            mHandler = handler;
233        }
234
235        private static class SpellCheckerParams {
236            public final int mWhat;
237            public final TextInfo[] mTextInfos;
238            public final int mSuggestionsLimit;
239            public final boolean mSequentialWords;
240            public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
241                    boolean sequentialWords) {
242                mWhat = what;
243                mTextInfos = textInfos;
244                mSuggestionsLimit = suggestionsLimit;
245                mSequentialWords = sequentialWords;
246            }
247        }
248
249        private void processTask(SpellCheckerParams scp) {
250            switch (scp.mWhat) {
251                case TASK_CANCEL:
252                    processCancel();
253                    break;
254                case TASK_GET_SUGGESTIONS_MULTIPLE:
255                    processGetSuggestionsMultiple(scp);
256                    break;
257                case TASK_CLOSE:
258                    processClose();
259                    break;
260                case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
261                    processGetSuggestionsMultipleForSentence(scp);
262                    break;
263            }
264        }
265
266        public synchronized void onServiceConnected(ISpellCheckerSession session) {
267            mISpellCheckerSession = session;
268            mOpened = true;
269            if (DBG)
270                Log.d(TAG, "onServiceConnected - Success");
271            while (!mPendingTasks.isEmpty()) {
272                processTask(mPendingTasks.poll());
273            }
274        }
275
276        public void cancel() {
277            if (DBG) {
278                Log.w(TAG, "cancel");
279            }
280            processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
281        }
282
283        public void getSuggestionsMultiple(
284                TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
285            if (DBG) {
286                Log.w(TAG, "getSuggestionsMultiple");
287            }
288            processOrEnqueueTask(
289                    new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
290                            suggestionsLimit, sequentialWords));
291        }
292
293        public void getSuggestionsMultipleForSentence(TextInfo[] textInfos, int suggestionsLimit) {
294            if (DBG) {
295                Log.w(TAG, "getSuggestionsMultipleForSentence");
296            }
297            processOrEnqueueTask(
298                    new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
299                            textInfos, suggestionsLimit, false));
300        }
301
302        public void close() {
303            if (DBG) {
304                Log.w(TAG, "close");
305            }
306            processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
307        }
308
309        public boolean isDisconnected() {
310            return mOpened && mISpellCheckerSession == null;
311        }
312
313        public boolean checkOpenConnection() {
314            if (mISpellCheckerSession != null) {
315                return true;
316            }
317            Log.e(TAG, "not connected to the spellchecker service.");
318            return false;
319        }
320
321        private void processOrEnqueueTask(SpellCheckerParams scp) {
322            if (DBG) {
323                Log.d(TAG, "process or enqueue task: " + mISpellCheckerSession);
324            }
325            SpellCheckerParams closeTask = null;
326            if (mISpellCheckerSession == null) {
327                if (scp.mWhat == TASK_CANCEL) {
328                    while (!mPendingTasks.isEmpty()) {
329                        final SpellCheckerParams tmp = mPendingTasks.poll();
330                        if (tmp.mWhat == TASK_CLOSE) {
331                            // Only one close task should be processed, while we need to remove all
332                            // close tasks from the queue
333                            closeTask = tmp;
334                        }
335                    }
336                }
337                mPendingTasks.offer(scp);
338                if (closeTask != null) {
339                    mPendingTasks.offer(closeTask);
340                }
341            } else {
342                processTask(scp);
343            }
344        }
345
346        private void processCancel() {
347            if (!checkOpenConnection()) {
348                return;
349            }
350            if (DBG) {
351                Log.w(TAG, "Cancel spell checker tasks.");
352            }
353            try {
354                mISpellCheckerSession.onCancel();
355            } catch (RemoteException e) {
356                Log.e(TAG, "Failed to cancel " + e);
357            }
358        }
359
360        private void processClose() {
361            if (!checkOpenConnection()) {
362                return;
363            }
364            if (DBG) {
365                Log.w(TAG, "Close spell checker tasks.");
366            }
367            try {
368                mISpellCheckerSession.onClose();
369                mISpellCheckerSession = null;
370                mHandler = null;
371            } catch (RemoteException e) {
372                Log.e(TAG, "Failed to close " + e);
373            }
374        }
375
376        private void processGetSuggestionsMultiple(SpellCheckerParams scp) {
377            if (!checkOpenConnection()) {
378                return;
379            }
380            if (DBG) {
381                Log.w(TAG, "Get suggestions from the spell checker.");
382            }
383            try {
384                mISpellCheckerSession.onGetSuggestionsMultiple(
385                        scp.mTextInfos, scp.mSuggestionsLimit, scp.mSequentialWords);
386            } catch (RemoteException e) {
387                Log.e(TAG, "Failed to get suggestions " + e);
388            }
389        }
390
391        private void processGetSuggestionsMultipleForSentence(SpellCheckerParams scp) {
392            if (!checkOpenConnection()) {
393                return;
394            }
395            if (DBG) {
396                Log.w(TAG, "Get suggestions from the spell checker.");
397            }
398            if (scp.mTextInfos.length != 1) {
399                throw new IllegalArgumentException();
400            }
401            try {
402                mISpellCheckerSession.onGetSuggestionsMultipleForSentence(
403                        scp.mTextInfos, scp.mSuggestionsLimit);
404            } catch (RemoteException e) {
405                Log.e(TAG, "Failed to get suggestions " + e);
406            }
407        }
408
409        @Override
410        public void onGetSuggestions(SuggestionsInfo[] results) {
411            mHandler.sendMessage(Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE, results));
412        }
413
414        @Override
415        public void onGetSuggestionsForSentence(SuggestionsInfo[] results) {
416            mHandler.sendMessage(
417                    Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
418        }
419    }
420
421    /**
422     * Callback for getting results from text services
423     */
424    public interface SpellCheckerSessionListener {
425        /**
426         * Callback for "getSuggestions"
427         * @param results an array of results of getSuggestions
428         */
429        public void onGetSuggestions(SuggestionsInfo[] results);
430        /**
431         * @hide
432         */
433        public void onGetSuggestionsForSentence(SuggestionsInfo[] results);
434    }
435
436    private static class InternalListener extends ITextServicesSessionListener.Stub {
437        private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
438
439        public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
440            mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
441        }
442
443        @Override
444        public void onServiceConnected(ISpellCheckerSession session) {
445            if (DBG) {
446                Log.w(TAG, "SpellCheckerSession connected.");
447            }
448            mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
449        }
450    }
451
452    @Override
453    protected void finalize() throws Throwable {
454        super.finalize();
455        if (mIsUsed) {
456            Log.e(TAG, "SpellCheckerSession was not finished properly." +
457                    "You should call finishShession() when you finished to use a spell checker.");
458            close();
459        }
460    }
461
462    /**
463     * @hide
464     */
465    public ITextServicesSessionListener getTextServicesSessionListener() {
466        return mInternalListener;
467    }
468
469    /**
470     * @hide
471     */
472    public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
473        return mSpellCheckerSessionListenerImpl;
474    }
475
476    /**
477     * @hide
478     */
479    public boolean isSentenceSpellCheckSupported() {
480        return mSubtype.containsExtraValueKey(SUPPORT_SENTENCE_SPELL_CHECK);
481    }
482}
483