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 
17 package android.view.textservice;
18 
19 import android.annotation.BinderThread;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SuppressLint;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.os.Binder;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.os.HandlerThread;
29 import android.os.Message;
30 import android.os.Process;
31 import android.os.RemoteException;
32 import android.util.Log;
33 import android.view.inputmethod.InputMethodManager;
34 
35 import com.android.internal.annotations.GuardedBy;
36 import com.android.internal.textservice.ISpellCheckerSession;
37 import com.android.internal.textservice.ISpellCheckerSessionListener;
38 import com.android.internal.textservice.ITextServicesSessionListener;
39 
40 import dalvik.system.CloseGuard;
41 
42 import java.util.ArrayDeque;
43 import java.util.Locale;
44 import java.util.Queue;
45 import java.util.concurrent.Executor;
46 
47 /**
48  * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService.
49  *
50  *
51  * <a name="Applications"></a>
52  * <h3>Applications</h3>
53  *
54  * <p>In most cases, applications that are using the standard
55  * {@link android.widget.TextView} or its subclasses will have little they need
56  * to do to work well with spell checker services.  The main things you need to
57  * be aware of are:</p>
58  *
59  * <ul>
60  * <li> Properly set the {@link android.R.attr#inputType} in your editable
61  * text views, so that the spell checker will have enough context to help the
62  * user in editing text in them.
63  * </ul>
64  *
65  * <p>For the rare people amongst us writing client applications that use the spell checker service
66  * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or
67  * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker
68  * service by yourself.</p>
69  *
70  * <h3>Security</h3>
71  *
72  * <p>There are a lot of security issues associated with spell checkers,
73  * since they could monitor all the text being sent to them
74  * through, for instance, {@link android.widget.TextView}.
75  * The Android spell checker framework also allows
76  * arbitrary third party spell checkers, so care must be taken to restrict their
77  * selection and interactions.</p>
78  *
79  * <p>Here are some key points about the security architecture behind the
80  * spell checker framework:</p>
81  *
82  * <ul>
83  * <li>Only the system is allowed to directly access a spell checker framework's
84  * {@link android.service.textservice.SpellCheckerService} interface, via the
85  * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission.  This is
86  * enforced in the system by not binding to a spell checker service that does
87  * not require this permission.
88  *
89  * <li>The user must explicitly enable a new spell checker in settings before
90  * they can be enabled, to confirm with the system that they know about it
91  * and want to make it available for use.
92  * </ul>
93  *
94  */
95 public class SpellCheckerSession {
96     private static final String TAG = SpellCheckerSession.class.getSimpleName();
97     private static final boolean DBG = false;
98     /**
99      * Name under which a SpellChecker service component publishes information about itself.
100      * This meta-data must reference an XML resource.
101      **/
102     public static final String SERVICE_META_DATA = "android.view.textservice.scs";
103 
104     private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
105     private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
106 
107     private final InternalListener mInternalListener;
108     private final TextServicesManager mTextServicesManager;
109     private final SpellCheckerInfo mSpellCheckerInfo;
110     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
111     private final SpellCheckerSessionListener mSpellCheckerSessionListener;
112     private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
113     private final Executor mExecutor;
114 
115     private final CloseGuard mGuard = CloseGuard.get();
116 
117     /**
118      * Constructor
119      * @hide
120      */
SpellCheckerSession( SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener, Executor executor)121     public SpellCheckerSession(
122             SpellCheckerInfo info, TextServicesManager tsm, SpellCheckerSessionListener listener,
123             Executor executor) {
124         if (info == null || listener == null || tsm == null) {
125             throw new NullPointerException();
126         }
127         mSpellCheckerInfo = info;
128         mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(this);
129         mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
130         mTextServicesManager = tsm;
131         mSpellCheckerSessionListener = listener;
132         mExecutor = executor;
133 
134         mGuard.open("finishSession");
135     }
136 
137     /**
138      * @return true if the connection to a text service of this session is disconnected and not
139      * alive.
140      */
isSessionDisconnected()141     public boolean isSessionDisconnected() {
142         return mSpellCheckerSessionListenerImpl.isDisconnected();
143     }
144 
145     /**
146      * Get the spell checker service info this spell checker session has.
147      * @return SpellCheckerInfo for the specified locale.
148      */
getSpellChecker()149     public SpellCheckerInfo getSpellChecker() {
150         return mSpellCheckerInfo;
151     }
152 
153     /**
154      * Cancel pending and running spell check tasks
155      */
cancel()156     public void cancel() {
157         mSpellCheckerSessionListenerImpl.cancel();
158     }
159 
160     /**
161      * Finish this session and allow TextServicesManagerService to disconnect the bound spell
162      * checker.
163      */
close()164     public void close() {
165         mGuard.close();
166         mSpellCheckerSessionListenerImpl.close();
167         mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
168     }
169 
170     /**
171      * Get suggestions from the specified sentences
172      * @param textInfos an array of text metadata for a spell checker
173      * @param suggestionsLimit the maximum number of suggestions that will be returned
174      */
getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit)175     public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) {
176         final InputMethodManager imm = mTextServicesManager.getInputMethodManager();
177         if (imm != null && imm.isInputMethodSuppressingSpellChecker()) {
178             handleOnGetSentenceSuggestionsMultiple(new SentenceSuggestionsInfo[0]);
179             return;
180         }
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
getSuggestions(TextInfo textInfo, int suggestionsLimit)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
getSuggestions( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)204     public void getSuggestions(
205             TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
206         if (DBG) {
207             Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
208         }
209         final InputMethodManager imm = mTextServicesManager.getInputMethodManager();
210         if (imm != null && imm.isInputMethodSuppressingSpellChecker()) {
211             handleOnGetSuggestionsMultiple(new SuggestionsInfo[0]);
212             return;
213         }
214         mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
215                 textInfos, suggestionsLimit, sequentialWords);
216     }
217 
handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionsInfos)218     void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionsInfos) {
219         mExecutor.execute(() -> mSpellCheckerSessionListener.onGetSuggestions(suggestionsInfos));
220     }
221 
handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionsInfos)222     void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionsInfos) {
223         mExecutor.execute(() ->
224                 mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionsInfos));
225     }
226 
227     private static final class SpellCheckerSessionListenerImpl
228             extends ISpellCheckerSessionListener.Stub {
229         private static final int TASK_CANCEL = 1;
230         private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
231         private static final int TASK_CLOSE = 3;
232         private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
taskToString(int task)233         private static String taskToString(int task) {
234             switch (task) {
235                 case TASK_CANCEL:
236                     return "TASK_CANCEL";
237                 case TASK_GET_SUGGESTIONS_MULTIPLE:
238                     return "TASK_GET_SUGGESTIONS_MULTIPLE";
239                 case TASK_CLOSE:
240                     return "TASK_CLOSE";
241                 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
242                     return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE";
243                 default:
244                     return "Unexpected task=" + task;
245             }
246         }
247 
248         private final Queue<SpellCheckerParams> mPendingTasks = new ArrayDeque<>();
249         @GuardedBy("SpellCheckerSessionListenerImpl.this")
250         private SpellCheckerSession mSpellCheckerSession;
251 
252         private static final int STATE_WAIT_CONNECTION = 0;
253         private static final int STATE_CONNECTED = 1;
254         private static final int STATE_CLOSED_AFTER_CONNECTION = 2;
255         private static final int STATE_CLOSED_BEFORE_CONNECTION = 3;
stateToString(int state)256         private static String stateToString(int state) {
257             switch (state) {
258                 case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION";
259                 case STATE_CONNECTED: return "STATE_CONNECTED";
260                 case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION";
261                 case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION";
262                 default: return "Unexpected state=" + state;
263             }
264         }
265         private int mState = STATE_WAIT_CONNECTION;
266 
267         private ISpellCheckerSession mISpellCheckerSession;
268         private HandlerThread mThread;
269         private Handler mAsyncHandler;
270 
SpellCheckerSessionListenerImpl(SpellCheckerSession spellCheckerSession)271         SpellCheckerSessionListenerImpl(SpellCheckerSession spellCheckerSession) {
272             mSpellCheckerSession = spellCheckerSession;
273         }
274 
275         private static class SpellCheckerParams {
276             public final int mWhat;
277             public final TextInfo[] mTextInfos;
278             public final int mSuggestionsLimit;
279             public final boolean mSequentialWords;
280             public ISpellCheckerSession mSession;
SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)281             public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
282                     boolean sequentialWords) {
283                 mWhat = what;
284                 mTextInfos = textInfos;
285                 mSuggestionsLimit = suggestionsLimit;
286                 mSequentialWords = sequentialWords;
287             }
288         }
289 
processTask(ISpellCheckerSession session, SpellCheckerParams scp, boolean async)290         private void processTask(ISpellCheckerSession session, SpellCheckerParams scp,
291                 boolean async) {
292             if (DBG) {
293                 synchronized (this) {
294                     Log.d(TAG, "entering processTask:"
295                             + " session.hashCode()=#" + Integer.toHexString(session.hashCode())
296                             + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async
297                             + " mAsyncHandler=" + mAsyncHandler
298                             + " mState=" + stateToString(mState));
299                 }
300             }
301             if (async || mAsyncHandler == null) {
302                 switch (scp.mWhat) {
303                     case TASK_CANCEL:
304                         try {
305                             session.onCancel();
306                         } catch (RemoteException e) {
307                             Log.e(TAG, "Failed to cancel " + e);
308                         }
309                         break;
310                     case TASK_GET_SUGGESTIONS_MULTIPLE:
311                         try {
312                             session.onGetSuggestionsMultiple(scp.mTextInfos,
313                                     scp.mSuggestionsLimit, scp.mSequentialWords);
314                         } catch (RemoteException e) {
315                             Log.e(TAG, "Failed to get suggestions " + e);
316                         }
317                         break;
318                     case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
319                         try {
320                             session.onGetSentenceSuggestionsMultiple(
321                                     scp.mTextInfos, scp.mSuggestionsLimit);
322                         } catch (RemoteException e) {
323                             Log.e(TAG, "Failed to get suggestions " + e);
324                         }
325                         break;
326                     case TASK_CLOSE:
327                         try {
328                             session.onClose();
329                         } catch (RemoteException e) {
330                             Log.e(TAG, "Failed to close " + e);
331                         }
332                         break;
333                 }
334             } else {
335                 // The interface is to a local object, so need to execute it
336                 // asynchronously.
337                 scp.mSession = session;
338                 mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp));
339             }
340 
341             if (scp.mWhat == TASK_CLOSE) {
342                 // If we are closing, we want to clean up our state now even
343                 // if it is pending as an async operation.
344                 synchronized (this) {
345                     processCloseLocked();
346                 }
347             }
348         }
349 
350         @GuardedBy("SpellCheckerSessionListenerImpl.this")
processCloseLocked()351         private void processCloseLocked() {
352             if (DBG) Log.d(TAG, "entering processCloseLocked:"
353                     + " session" + (mISpellCheckerSession != null ? ".hashCode()=#"
354                             + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null")
355                     + " mState=" + stateToString(mState));
356             mISpellCheckerSession = null;
357             if (mThread != null) {
358                 mThread.quit();
359             }
360             mSpellCheckerSession = null;
361             mPendingTasks.clear();
362             mThread = null;
363             mAsyncHandler = null;
364             switch (mState) {
365                 case STATE_WAIT_CONNECTION:
366                     mState = STATE_CLOSED_BEFORE_CONNECTION;
367                     break;
368                 case STATE_CONNECTED:
369                     mState = STATE_CLOSED_AFTER_CONNECTION;
370                     break;
371                 default:
372                     Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" +
373                             stateToString(mState));
374                     break;
375             }
376         }
377 
onServiceConnected(ISpellCheckerSession session)378         public void onServiceConnected(ISpellCheckerSession session) {
379             synchronized (this) {
380                 switch (mState) {
381                     case STATE_WAIT_CONNECTION:
382                         // OK, go ahead.
383                         break;
384                     case STATE_CLOSED_BEFORE_CONNECTION:
385                         // This is possible, and not an error.  The client no longer is interested
386                         // in this connection. OK to ignore.
387                         if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is"
388                                 + " already closed.");
389                         return;
390                     default:
391                         Log.e(TAG, "ignoring onServiceConnected due to unexpected mState="
392                                 + stateToString(mState));
393                         return;
394                 }
395                 if (session == null) {
396                     Log.e(TAG, "ignoring onServiceConnected due to session=null");
397                     return;
398                 }
399                 mISpellCheckerSession = session;
400                 if (session.asBinder() instanceof Binder && mThread == null) {
401                     if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected.");
402                     // If this is a local object, we need to do our own threading
403                     // to make sure we handle it asynchronously.
404                     mThread = new HandlerThread("SpellCheckerSession",
405                             Process.THREAD_PRIORITY_BACKGROUND);
406                     mThread.start();
407                     mAsyncHandler = new Handler(mThread.getLooper()) {
408                         @Override public void handleMessage(Message msg) {
409                             SpellCheckerParams scp = (SpellCheckerParams)msg.obj;
410                             processTask(scp.mSession, scp, true);
411                         }
412                     };
413                 }
414                 mState = STATE_CONNECTED;
415                 if (DBG) {
416                     Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#"
417                             + Integer.toHexString(mISpellCheckerSession.hashCode())
418                             + " mPendingTasks.size()=" + mPendingTasks.size());
419                 }
420                 while (!mPendingTasks.isEmpty()) {
421                     processTask(session, mPendingTasks.poll(), false);
422                 }
423             }
424         }
425 
cancel()426         public void cancel() {
427             processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
428         }
429 
getSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords)430         public void getSuggestionsMultiple(
431                 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
432             processOrEnqueueTask(
433                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
434                             suggestionsLimit, sequentialWords));
435         }
436 
getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit)437         public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
438             processOrEnqueueTask(
439                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
440                             textInfos, suggestionsLimit, false));
441         }
442 
close()443         public void close() {
444             processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
445         }
446 
isDisconnected()447         public boolean isDisconnected() {
448             synchronized (this) {
449                 return mState != STATE_CONNECTED;
450             }
451         }
452 
processOrEnqueueTask(SpellCheckerParams scp)453         private void processOrEnqueueTask(SpellCheckerParams scp) {
454             ISpellCheckerSession session;
455             synchronized (this) {
456                 if (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION
457                         || mState == STATE_CLOSED_BEFORE_CONNECTION)) {
458                     // It is OK to call SpellCheckerSession#close() multiple times.
459                     // Don't output confusing/misleading warning messages.
460                     return;
461                 }
462                 if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) {
463                     Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState="
464                             + stateToString(mState)
465                             + " scp.mWhat=" + taskToString(scp.mWhat));
466                     return;
467                 }
468 
469                 if (mState == STATE_WAIT_CONNECTION) {
470                     // If we are still waiting for the connection. Need to pay special attention.
471                     if (scp.mWhat == TASK_CLOSE) {
472                         processCloseLocked();
473                         return;
474                     }
475                     // Enqueue the task to task queue.
476                     SpellCheckerParams closeTask = null;
477                     if (scp.mWhat == TASK_CANCEL) {
478                         if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask.");
479                         while (!mPendingTasks.isEmpty()) {
480                             final SpellCheckerParams tmp = mPendingTasks.poll();
481                             if (tmp.mWhat == TASK_CLOSE) {
482                                 // Only one close task should be processed, while we need to remove
483                                 // all close tasks from the queue
484                                 closeTask = tmp;
485                             }
486                         }
487                     }
488                     mPendingTasks.offer(scp);
489                     if (closeTask != null) {
490                         mPendingTasks.offer(closeTask);
491                     }
492                     if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the"
493                             + " connection is not established."
494                             + " mPendingTasks.size()=" + mPendingTasks.size());
495                     return;
496                 }
497 
498                 session = mISpellCheckerSession;
499             }
500             // session must never be null here.
501             processTask(session, scp, false);
502         }
503 
504         @BinderThread
505         @Override
onGetSuggestions(SuggestionsInfo[] results)506         public void onGetSuggestions(SuggestionsInfo[] results) {
507             SpellCheckerSession session = getSpellCheckerSession();
508             if (session != null) {
509                 // Lock should not be held when calling callback, in order to avoid deadlock.
510                 session.handleOnGetSuggestionsMultiple(results);
511             }
512         }
513 
514         @BinderThread
515         @Override
onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)516         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
517             SpellCheckerSession session = getSpellCheckerSession();
518             if (session != null) {
519                 // Lock should not be held when calling callback, in order to avoid deadlock.
520                 session.handleOnGetSentenceSuggestionsMultiple(results);
521             }
522         }
523 
524         @Nullable
getSpellCheckerSession()525         private SpellCheckerSession getSpellCheckerSession() {
526             synchronized (SpellCheckerSessionListenerImpl.this) {
527                 return mSpellCheckerSession;
528             }
529         }
530     }
531 
532     /** Parameters used to create a {@link SpellCheckerSession}. */
533     public static class SpellCheckerSessionParams {
534         @Nullable
535         private final Locale mLocale;
536         private final boolean mShouldReferToSpellCheckerLanguageSettings;
537         private final @SuggestionsInfo.ResultAttrs int mSupportedAttributes;
538         private final Bundle mExtras;
539 
SpellCheckerSessionParams(Locale locale, boolean referToSpellCheckerLanguageSettings, int supportedAttributes, Bundle extras)540         private SpellCheckerSessionParams(Locale locale,
541                 boolean referToSpellCheckerLanguageSettings, int supportedAttributes,
542                 Bundle extras) {
543             mLocale = locale;
544             mShouldReferToSpellCheckerLanguageSettings = referToSpellCheckerLanguageSettings;
545             mSupportedAttributes = supportedAttributes;
546             mExtras = extras;
547         }
548 
549         /**
550          * Returns the locale in which the spell checker should operate.
551          *
552          * @see android.service.textservice.SpellCheckerService.Session#getLocale()
553          */
554         @SuppressLint("UseIcu")
555         @Nullable
getLocale()556         public Locale getLocale() {
557             return mLocale;
558         }
559 
560         /**
561          * Returns true if the user's spell checker language settings should be used to determine
562          * the spell checker locale.
563          */
shouldReferToSpellCheckerLanguageSettings()564         public boolean shouldReferToSpellCheckerLanguageSettings() {
565             return mShouldReferToSpellCheckerLanguageSettings;
566         }
567 
568         /**
569          * Returns a bitmask of {@link SuggestionsInfo} attributes that the spell checker can set
570          * in {@link SuggestionsInfo} it returns.
571          *
572          * @see android.service.textservice.SpellCheckerService.Session#getSupportedAttributes()
573          */
getSupportedAttributes()574         public @SuggestionsInfo.ResultAttrs int getSupportedAttributes() {
575             return mSupportedAttributes;
576         }
577 
578         /**
579          * Returns a bundle containing extra parameters for the spell checker.
580          *
581          * <p>This bundle can be used to pass implementation-specific parameters to the
582          * {@link android.service.textservice.SpellCheckerService} implementation.
583          *
584          * @see android.service.textservice.SpellCheckerService.Session#getBundle()
585          */
586         @NonNull
getExtras()587         public Bundle getExtras() {
588             return mExtras;
589         }
590 
591         /** Builder of {@link SpellCheckerSessionParams}. */
592         public static final class Builder {
593             @Nullable
594             private Locale mLocale;
595             private boolean mShouldReferToSpellCheckerLanguageSettings = false;
596             private @SuggestionsInfo.ResultAttrs int mSupportedAttributes = 0;
597             private Bundle mExtras = Bundle.EMPTY;
598 
599             /** Constructs a {@code Builder}. */
Builder()600             public Builder() {
601             }
602 
603             /**
604              * Returns constructed {@link SpellCheckerSession} instance.
605              *
606              * <p>Before calling this method, either {@link #setLocale(Locale)} should be called
607              * with a non-null locale or
608              * {@link #setShouldReferToSpellCheckerLanguageSettings(boolean)} should be called with
609              * {@code true}.
610              */
611             @NonNull
build()612             public SpellCheckerSessionParams build() {
613                 if (mLocale == null && !mShouldReferToSpellCheckerLanguageSettings) {
614                     throw new IllegalArgumentException("mLocale should not be null if "
615                             + " mShouldReferToSpellCheckerLanguageSettings is false.");
616                 }
617                 return new SpellCheckerSessionParams(mLocale,
618                         mShouldReferToSpellCheckerLanguageSettings, mSupportedAttributes, mExtras);
619             }
620 
621             /**
622              * Sets the locale in which the spell checker should operate.
623              *
624              * @see android.service.textservice.SpellCheckerService.Session#getLocale()
625              */
626             @NonNull
setLocale(@uppressLint"UseIcu") @ullable Locale locale)627             public Builder setLocale(@SuppressLint("UseIcu") @Nullable Locale locale) {
628                 mLocale = locale;
629                 return this;
630             }
631 
632             /**
633              * Sets whether or not the user's spell checker language settings should be used to
634              * determine spell checker locale.
635              *
636              * <p>If {@code shouldReferToSpellCheckerLanguageSettings} is true, the exact way of
637              * determining spell checker locale differs based on {@code locale} specified in
638              * {@link #setLocale(Locale)}.
639              * If {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is
640              * null, the locale specified in Settings will be used. If
641              * {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is not
642              * null, {@link SpellCheckerSession} can be created only when the locale specified in
643              * Settings is the same as {@code locale}. Exceptionally, if
644              * {@code shouldReferToSpellCheckerLanguageSettings} is true and {@code locale} is
645              * language only (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be
646              * used.
647              *
648              * @see #setLocale(Locale)
649              */
650             @NonNull
setShouldReferToSpellCheckerLanguageSettings( boolean shouldReferToSpellCheckerLanguageSettings)651             public Builder setShouldReferToSpellCheckerLanguageSettings(
652                     boolean shouldReferToSpellCheckerLanguageSettings) {
653                 mShouldReferToSpellCheckerLanguageSettings =
654                         shouldReferToSpellCheckerLanguageSettings;
655                 return this;
656             }
657 
658             /**
659              * Sets a bitmask of {@link SuggestionsInfo} attributes that the spell checker can set
660              * in {@link SuggestionsInfo} it returns.
661              *
662              * @see android.service.textservice.SpellCheckerService.Session#getSupportedAttributes()
663              */
664             @NonNull
setSupportedAttributes( @uggestionsInfo.ResultAttrs int supportedAttributes)665             public Builder setSupportedAttributes(
666                     @SuggestionsInfo.ResultAttrs int supportedAttributes) {
667                 mSupportedAttributes = supportedAttributes;
668                 return this;
669             }
670 
671             /**
672              * Sets a bundle containing extra parameters for the spell checker.
673              *
674              * <p>This bundle can be used to pass implementation-specific parameters to the
675              * {@link android.service.textservice.SpellCheckerService} implementation.
676              *
677              * @see android.service.textservice.SpellCheckerService.Session#getBundle()
678              */
679             @NonNull
setExtras(@onNull Bundle extras)680             public Builder setExtras(@NonNull Bundle extras) {
681                 mExtras = extras;
682                 return this;
683             }
684         }
685     }
686 
687     /**
688      * Callback for getting results from text services
689      */
690     public interface SpellCheckerSessionListener {
691         /**
692          * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)}
693          * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
694          * @param results an array of {@link SuggestionsInfo}s.
695          * These results are suggestions for {@link TextInfo}s queried by
696          * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or
697          * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
698          */
onGetSuggestions(SuggestionsInfo[] results)699         public void onGetSuggestions(SuggestionsInfo[] results);
700         /**
701          * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
702          * @param results an array of {@link SentenceSuggestionsInfo}s.
703          * These results are suggestions for {@link TextInfo}s
704          * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
705          */
onGetSentenceSuggestions(SentenceSuggestionsInfo[] results)706         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
707     }
708 
709     private static final class InternalListener extends ITextServicesSessionListener.Stub {
710         private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
711 
InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl)712         public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
713             mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
714         }
715 
716         @Override
onServiceConnected(ISpellCheckerSession session)717         public void onServiceConnected(ISpellCheckerSession session) {
718             mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
719         }
720     }
721 
722     @Override
finalize()723     protected void finalize() throws Throwable {
724         try {
725             // Note that mGuard will be null if the constructor threw.
726             if (mGuard != null) {
727                 mGuard.warnIfOpen();
728                 close();
729             }
730         } finally {
731             super.finalize();
732         }
733     }
734 
735     /**
736      * @hide
737      */
getTextServicesSessionListener()738     public ITextServicesSessionListener getTextServicesSessionListener() {
739         return mInternalListener;
740     }
741 
742     /**
743      * @hide
744      */
getSpellCheckerSessionListener()745     public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
746         return mSpellCheckerSessionListenerImpl;
747     }
748 }
749