SpellCheckerSession.java revision 44b75030931d9c65c9e495a86d11d71da59b4429
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
92
93    private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
94
95    private final InternalListener mInternalListener;
96    private final ITextServicesManager mTextServicesManager;
97    private final SpellCheckerInfo mSpellCheckerInfo;
98    private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
99
100    private boolean mIsUsed;
101    private SpellCheckerSessionListener mSpellCheckerSessionListener;
102
103    /** Handler that will execute the main tasks */
104    private final Handler mHandler = new Handler() {
105        @Override
106        public void handleMessage(Message msg) {
107            switch (msg.what) {
108                case MSG_ON_GET_SUGGESTION_MULTIPLE:
109                    handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj);
110                    break;
111            }
112        }
113    };
114
115    /**
116     * Constructor
117     * @hide
118     */
119    public SpellCheckerSession(
120            SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) {
121        if (info == null || listener == null || tsm == null) {
122            throw new NullPointerException();
123        }
124        mSpellCheckerInfo = info;
125        mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler);
126        mInternalListener = new InternalListener();
127        mTextServicesManager = tsm;
128        mIsUsed = true;
129        mSpellCheckerSessionListener = listener;
130    }
131
132    /**
133     * @return true if the connection to a text service of this session is disconnected and not
134     * alive.
135     */
136    public boolean isSessionDisconnected() {
137        return mSpellCheckerSessionListenerImpl.isDisconnected();
138    }
139
140    /**
141     * Get the spell checker service info this spell checker session has.
142     * @return SpellCheckerInfo for the specified locale.
143     */
144    public SpellCheckerInfo getSpellChecker() {
145        return mSpellCheckerInfo;
146    }
147
148    /**
149     * Finish this session and allow TextServicesManagerService to disconnect the bound spell
150     * checker.
151     */
152    public void close() {
153        mIsUsed = false;
154        try {
155            mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
156        } catch (RemoteException e) {
157            // do nothing
158        }
159    }
160
161    /**
162     * Get candidate strings for a substring of the specified text.
163     * @param textInfo text metadata for a spell checker
164     * @param suggestionsLimit the number of limit of suggestions returned
165     */
166    public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
167        getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
168    }
169
170    /**
171     * A batch process of getSuggestions
172     * @param textInfos an array of text metadata for a spell checker
173     * @param suggestionsLimit the number of limit of suggestions returned
174     * @param sequentialWords true if textInfos can be treated as sequential words.
175     */
176    public void getSuggestions(
177            TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
178        if (DBG) {
179            Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
180        }
181        // TODO: Handle multiple words suggestions by using WordBreakIterator
182        mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
183                textInfos, suggestionsLimit, sequentialWords);
184    }
185
186    private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) {
187        mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
188    }
189
190    private static class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub {
191        private static final int TASK_CANCEL = 1;
192        private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
193        private final Queue<SpellCheckerParams> mPendingTasks =
194                new LinkedList<SpellCheckerParams>();
195        private final Handler mHandler;
196
197        private boolean mOpened;
198        private ISpellCheckerSession mISpellCheckerSession;
199
200        public SpellCheckerSessionListenerImpl(Handler handler) {
201            mOpened = false;
202            mHandler = handler;
203        }
204
205        private static class SpellCheckerParams {
206            public final int mWhat;
207            public final TextInfo[] mTextInfos;
208            public final int mSuggestionsLimit;
209            public final boolean mSequentialWords;
210            public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
211                    boolean sequentialWords) {
212                mWhat = what;
213                mTextInfos = textInfos;
214                mSuggestionsLimit = suggestionsLimit;
215                mSequentialWords = sequentialWords;
216            }
217        }
218
219        private void processTask(SpellCheckerParams scp) {
220            switch (scp.mWhat) {
221                case TASK_CANCEL:
222                    processCancel();
223                    break;
224                case TASK_GET_SUGGESTIONS_MULTIPLE:
225                    processGetSuggestionsMultiple(scp);
226                    break;
227            }
228        }
229
230        public synchronized void onServiceConnected(ISpellCheckerSession session) {
231            mISpellCheckerSession = session;
232            mOpened = true;
233            if (DBG)
234                Log.d(TAG, "onServiceConnected - Success");
235            while (!mPendingTasks.isEmpty()) {
236                processTask(mPendingTasks.poll());
237            }
238        }
239
240        public void getSuggestionsMultiple(
241                TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
242            if (DBG) {
243                Log.w(TAG, "getSuggestionsMultiple");
244            }
245            processOrEnqueueTask(
246                    new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
247                            suggestionsLimit, sequentialWords));
248        }
249
250        public boolean isDisconnected() {
251            return mOpened && mISpellCheckerSession == null;
252        }
253
254        public boolean checkOpenConnection() {
255            if (mISpellCheckerSession != null) {
256                return true;
257            }
258            Log.e(TAG, "not connected to the spellchecker service.");
259            return false;
260        }
261
262        private void processOrEnqueueTask(SpellCheckerParams scp) {
263            if (DBG) {
264                Log.d(TAG, "process or enqueue task: " + mISpellCheckerSession);
265            }
266            if (mISpellCheckerSession == null) {
267                mPendingTasks.offer(scp);
268            } else {
269                processTask(scp);
270            }
271        }
272
273        private void processCancel() {
274            if (!checkOpenConnection()) {
275                return;
276            }
277            if (DBG) {
278                Log.w(TAG, "Cancel spell checker tasks.");
279            }
280            try {
281                mISpellCheckerSession.onCancel();
282            } catch (RemoteException e) {
283                Log.e(TAG, "Failed to cancel " + e);
284            }
285        }
286
287        private void processGetSuggestionsMultiple(SpellCheckerParams scp) {
288            if (!checkOpenConnection()) {
289                return;
290            }
291            if (DBG) {
292                Log.w(TAG, "Get suggestions from the spell checker.");
293            }
294            try {
295                mISpellCheckerSession.onGetSuggestionsMultiple(
296                        scp.mTextInfos, scp.mSuggestionsLimit, scp.mSequentialWords);
297            } catch (RemoteException e) {
298                Log.e(TAG, "Failed to get suggestions " + e);
299            }
300        }
301
302        @Override
303        public void onGetSuggestions(SuggestionsInfo[] results) {
304            mHandler.sendMessage(Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE, results));
305        }
306    }
307
308    /**
309     * Callback for getting results from text services
310     */
311    public interface SpellCheckerSessionListener {
312        /**
313         * Callback for "getSuggestions"
314         * @param results an array of results of getSuggestions
315         */
316        public void onGetSuggestions(SuggestionsInfo[] results);
317    }
318
319    private class InternalListener extends ITextServicesSessionListener.Stub {
320        @Override
321        public void onServiceConnected(ISpellCheckerSession session) {
322            if (DBG) {
323                Log.w(TAG, "SpellCheckerSession connected.");
324            }
325            mSpellCheckerSessionListenerImpl.onServiceConnected(session);
326        }
327    }
328
329    @Override
330    protected void finalize() throws Throwable {
331        super.finalize();
332        if (mIsUsed) {
333            Log.e(TAG, "SpellCheckerSession was not finished properly." +
334                    "You should call finishShession() when you finished to use a spell checker.");
335            close();
336        }
337    }
338
339    /**
340     * @hide
341     */
342    public ITextServicesSessionListener getTextServicesSessionListener() {
343        return mInternalListener;
344    }
345
346    /**
347     * @hide
348     */
349    public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
350        return mSpellCheckerSessionListenerImpl;
351    }
352}
353