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