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