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
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     */
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     */
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     */
153    public SpellCheckerInfo getSpellChecker() {
154        return mSpellCheckerInfo;
155    }
156
157    /**
158     * Cancel pending and running spell check tasks
159     */
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     */
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     */
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
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
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
216    private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) {
217        mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
218    }
219
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
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;
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
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
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 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
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
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
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 onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
430            synchronized (this) {
431                if (mHandler != null) {
432                    mHandler.sendMessage(Message.obtain(mHandler,
433                            MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
434                }
435            }
436        }
437    }
438
439    /**
440     * Callback for getting results from text services
441     */
442    public interface SpellCheckerSessionListener {
443        /**
444         * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)}
445         * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
446         * @param results an array of {@link SuggestionsInfo}s.
447         * These results are suggestions for {@link TextInfo}s queried by
448         * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or
449         * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
450         */
451        public void onGetSuggestions(SuggestionsInfo[] results);
452        /**
453         * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
454         * @param results an array of {@link SentenceSuggestionsInfo}s.
455         * These results are suggestions for {@link TextInfo}s
456         * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
457         */
458        public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
459    }
460
461    private static class InternalListener extends ITextServicesSessionListener.Stub {
462        private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
463
464        public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
465            mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
466        }
467
468        @Override
469        public void onServiceConnected(ISpellCheckerSession session) {
470            if (DBG) {
471                Log.w(TAG, "SpellCheckerSession connected.");
472            }
473            mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
474        }
475    }
476
477    @Override
478    protected void finalize() throws Throwable {
479        super.finalize();
480        if (mIsUsed) {
481            Log.e(TAG, "SpellCheckerSession was not finished properly." +
482                    "You should call finishShession() when you finished to use a spell checker.");
483            close();
484        }
485    }
486
487    /**
488     * @hide
489     */
490    public ITextServicesSessionListener getTextServicesSessionListener() {
491        return mInternalListener;
492    }
493
494    /**
495     * @hide
496     */
497    public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
498        return mSpellCheckerSessionListenerImpl;
499    }
500}
501