blob: f6418ce6f1cedb675884dc1430cfa45e02daaf71 [file] [log] [blame]
satok988323c2011-06-22 16:38:13 +09001/*
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
satokaafd9552011-08-02 15:24:00 +090017package android.view.textservice;
satok988323c2011-06-22 16:38:13 +090018
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
Dianne Hackborn33b8ee52011-12-13 15:08:40 -080024import android.os.Binder;
satok988323c2011-06-22 16:38:13 +090025import android.os.Handler;
Dianne Hackborn33b8ee52011-12-13 15:08:40 -080026import android.os.HandlerThread;
satok988323c2011-06-22 16:38:13 +090027import android.os.Message;
Dianne Hackborn33b8ee52011-12-13 15:08:40 -080028import android.os.Process;
satok988323c2011-06-22 16:38:13 +090029import android.os.RemoteException;
30import android.util.Log;
satok1bedd992011-07-23 11:39:55 +090031import android.view.textservice.SpellCheckerInfo;
satok988323c2011-06-22 16:38:13 +090032import 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.
satok44b75032011-10-14 14:48:59 +090040 *
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 *
satok988323c2011-06-22 16:38:13 +090085 */
86public class SpellCheckerSession {
87 private static final String TAG = SpellCheckerSession.class.getSimpleName();
88 private static final boolean DBG = false;
satokaafd9552011-08-02 15:24:00 +090089 /**
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";
satok0dc1f642011-11-18 11:27:10 +090094 private static final String SUPPORT_SENTENCE_SPELL_CHECK = "SupportSentenceSpellCheck";
satokaafd9552011-08-02 15:24:00 +090095
satok988323c2011-06-22 16:38:13 +090096
97 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
satok0dc1f642011-11-18 11:27:10 +090098 private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
satok988323c2011-06-22 16:38:13 +090099
100 private final InternalListener mInternalListener;
101 private final ITextServicesManager mTextServicesManager;
satok1bedd992011-07-23 11:39:55 +0900102 private final SpellCheckerInfo mSpellCheckerInfo;
satok988323c2011-06-22 16:38:13 +0900103 private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
satok0dc1f642011-11-18 11:27:10 +0900104 private final SpellCheckerSubtype mSubtype;
satok988323c2011-06-22 16:38:13 +0900105
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;
satok0dc1f642011-11-18 11:27:10 +0900117 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE:
118 handleOnGetSuggestionsMultipleForSentence((SuggestionsInfo[]) msg.obj);
119 break;
satok988323c2011-06-22 16:38:13 +0900120 }
121 }
122 };
123
124 /**
125 * Constructor
126 * @hide
127 */
satok1bedd992011-07-23 11:39:55 +0900128 public SpellCheckerSession(
satok0dc1f642011-11-18 11:27:10 +0900129 SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener,
130 SpellCheckerSubtype subtype) {
satok1bedd992011-07-23 11:39:55 +0900131 if (info == null || listener == null || tsm == null) {
satok988323c2011-06-22 16:38:13 +0900132 throw new NullPointerException();
133 }
satok1bedd992011-07-23 11:39:55 +0900134 mSpellCheckerInfo = info;
satok988323c2011-06-22 16:38:13 +0900135 mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler);
Jean Chalarda80838d2011-10-20 19:33:53 +0900136 mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
satok988323c2011-06-22 16:38:13 +0900137 mTextServicesManager = tsm;
138 mIsUsed = true;
139 mSpellCheckerSessionListener = listener;
satok0dc1f642011-11-18 11:27:10 +0900140 mSubtype = subtype;
satok988323c2011-06-22 16:38:13 +0900141 }
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 /**
satok1bedd992011-07-23 11:39:55 +0900152 * 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 /**
satokb4aff972011-11-03 04:12:51 +0900160 * Cancel pending and running spell check tasks
161 */
162 public void cancel() {
163 mSpellCheckerSessionListenerImpl.cancel();
164 }
165
166 /**
satok988323c2011-06-22 16:38:13 +0900167 * Finish this session and allow TextServicesManagerService to disconnect the bound spell
168 * checker.
169 */
170 public void close() {
171 mIsUsed = false;
172 try {
satok74061ff2011-11-02 11:20:33 +0900173 mSpellCheckerSessionListenerImpl.close();
satok988323c2011-06-22 16:38:13 +0900174 mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
175 } catch (RemoteException e) {
176 // do nothing
177 }
178 }
179
180 /**
satok0dc1f642011-11-18 11:27:10 +0900181 * @hide
182 */
183 public void getSuggestionsForSentence(TextInfo textInfo, int suggestionsLimit) {
184 mSpellCheckerSessionListenerImpl.getSuggestionsMultipleForSentence(
185 new TextInfo[] {textInfo}, suggestionsLimit);
186 }
187
188 /**
satok988323c2011-06-22 16:38:13 +0900189 * 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) {
satok6be6d752011-07-28 20:40:38 +0900205 if (DBG) {
206 Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
207 }
satok988323c2011-06-22 16:38:13 +0900208 // 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
satok0dc1f642011-11-18 11:27:10 +0900217 private void handleOnGetSuggestionsMultipleForSentence(SuggestionsInfo[] suggestionInfos) {
218 mSpellCheckerSessionListener.onGetSuggestionsForSentence(suggestionInfos);
219 }
220
satok988323c2011-06-22 16:38:13 +0900221 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;
satok74061ff2011-11-02 11:20:33 +0900224 private static final int TASK_CLOSE = 3;
satok0dc1f642011-11-18 11:27:10 +0900225 private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
satok988323c2011-06-22 16:38:13 +0900226 private final Queue<SpellCheckerParams> mPendingTasks =
227 new LinkedList<SpellCheckerParams>();
satok060677f2011-11-17 09:40:56 +0900228 private Handler mHandler;
satok988323c2011-06-22 16:38:13 +0900229
230 private boolean mOpened;
231 private ISpellCheckerSession mISpellCheckerSession;
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800232 private HandlerThread mThread;
233 private Handler mAsyncHandler;
satok988323c2011-06-22 16:38:13 +0900234
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;
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800245 public ISpellCheckerSession mSession;
satok988323c2011-06-22 16:38:13 +0900246 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
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800255 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;
Dianne Hackbornb5052de2011-12-13 16:31:43 -0800280 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;
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800294 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 }
satok988323c2011-06-22 16:38:13 +0900324 }
325 }
326
327 public synchronized void onServiceConnected(ISpellCheckerSession session) {
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800328 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 }
satok988323c2011-06-22 16:38:13 +0900345 if (DBG)
346 Log.d(TAG, "onServiceConnected - Success");
347 while (!mPendingTasks.isEmpty()) {
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800348 processTask(session, mPendingTasks.poll(), false);
satok988323c2011-06-22 16:38:13 +0900349 }
350 }
351
satokb4aff972011-11-03 04:12:51 +0900352 public void cancel() {
353 if (DBG) {
354 Log.w(TAG, "cancel");
355 }
356 processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
357 }
358
satok988323c2011-06-22 16:38:13 +0900359 public void getSuggestionsMultiple(
360 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
satok6be6d752011-07-28 20:40:38 +0900361 if (DBG) {
362 Log.w(TAG, "getSuggestionsMultiple");
363 }
satok988323c2011-06-22 16:38:13 +0900364 processOrEnqueueTask(
365 new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
366 suggestionsLimit, sequentialWords));
367 }
368
satok0dc1f642011-11-18 11:27:10 +0900369 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
satok74061ff2011-11-02 11:20:33 +0900378 public void close() {
379 if (DBG) {
380 Log.w(TAG, "close");
381 }
382 processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
383 }
384
satok988323c2011-06-22 16:38:13 +0900385 public boolean isDisconnected() {
386 return mOpened && mISpellCheckerSession == null;
387 }
388
satok988323c2011-06-22 16:38:13 +0900389 private void processOrEnqueueTask(SpellCheckerParams scp) {
satok6be6d752011-07-28 20:40:38 +0900390 if (DBG) {
391 Log.d(TAG, "process or enqueue task: " + mISpellCheckerSession);
392 }
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800393 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 }
satokb4aff972011-11-03 04:12:51 +0900406 }
407 }
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800408 mPendingTasks.offer(scp);
409 if (closeTask != null) {
410 mPendingTasks.offer(closeTask);
411 }
412 return;
satokb4aff972011-11-03 04:12:51 +0900413 }
satok988323c2011-06-22 16:38:13 +0900414 }
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800415 processTask(session, scp, false);
satok0dc1f642011-11-18 11:27:10 +0900416 }
417
satok988323c2011-06-22 16:38:13 +0900418 @Override
419 public void onGetSuggestions(SuggestionsInfo[] results) {
Dianne Hackborn33b8ee52011-12-13 15:08:40 -0800420 synchronized (this) {
421 if (mHandler != null) {
422 mHandler.sendMessage(Message.obtain(mHandler,
423 MSG_ON_GET_SUGGESTION_MULTIPLE, results));
424 }
425 }
satok988323c2011-06-22 16:38:13 +0900426 }
satok0dc1f642011-11-18 11:27:10 +0900427
428 @Override
429 public void onGetSuggestionsForSentence(SuggestionsInfo[] results) {
430 mHandler.sendMessage(
431 Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
432 }
satok988323c2011-06-22 16:38:13 +0900433 }
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);
satok0dc1f642011-11-18 11:27:10 +0900444 /**
445 * @hide
446 */
447 public void onGetSuggestionsForSentence(SuggestionsInfo[] results);
satok988323c2011-06-22 16:38:13 +0900448 }
449
Jean Chalarda80838d2011-10-20 19:33:53 +0900450 private static class InternalListener extends ITextServicesSessionListener.Stub {
451 private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
452
453 public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
454 mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
455 }
456
satok988323c2011-06-22 16:38:13 +0900457 @Override
458 public void onServiceConnected(ISpellCheckerSession session) {
satok6be6d752011-07-28 20:40:38 +0900459 if (DBG) {
460 Log.w(TAG, "SpellCheckerSession connected.");
461 }
Jean Chalarda80838d2011-10-20 19:33:53 +0900462 mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
satok988323c2011-06-22 16:38:13 +0900463 }
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 }
satok0dc1f642011-11-18 11:27:10 +0900489
490 /**
491 * @hide
492 */
493 public boolean isSentenceSpellCheckSupported() {
494 return mSubtype.containsExtraValueKey(SUPPORT_SENTENCE_SPELL_CHECK);
495 }
satok988323c2011-06-22 16:38:13 +0900496}