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 dalvik.system.CloseGuard;
     33 
     34 import java.util.LinkedList;
     35 import java.util.Queue;
     36 
     37 /**
     38  * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService.
     39  *
     40  *
     41  * <a name="Applications"></a>
     42  * <h3>Applications</h3>
     43  *
     44  * <p>In most cases, applications that are using the standard
     45  * {@link android.widget.TextView} or its subclasses will have little they need
     46  * to do to work well with spell checker services.  The main things you need to
     47  * be aware of are:</p>
     48  *
     49  * <ul>
     50  * <li> Properly set the {@link android.R.attr#inputType} in your editable
     51  * text views, so that the spell checker will have enough context to help the
     52  * user in editing text in them.
     53  * </ul>
     54  *
     55  * <p>For the rare people amongst us writing client applications that use the spell checker service
     56  * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or
     57  * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker
     58  * service by yourself.</p>
     59  *
     60  * <h3>Security</h3>
     61  *
     62  * <p>There are a lot of security issues associated with spell checkers,
     63  * since they could monitor all the text being sent to them
     64  * through, for instance, {@link android.widget.TextView}.
     65  * The Android spell checker framework also allows
     66  * arbitrary third party spell checkers, so care must be taken to restrict their
     67  * selection and interactions.</p>
     68  *
     69  * <p>Here are some key points about the security architecture behind the
     70  * spell checker framework:</p>
     71  *
     72  * <ul>
     73  * <li>Only the system is allowed to directly access a spell checker framework's
     74  * {@link android.service.textservice.SpellCheckerService} interface, via the
     75  * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission.  This is
     76  * enforced in the system by not binding to a spell checker service that does
     77  * not require this permission.
     78  *
     79  * <li>The user must explicitly enable a new spell checker in settings before
     80  * they can be enabled, to confirm with the system that they know about it
     81  * and want to make it available for use.
     82  * </ul>
     83  *
     84  */
     85 public class SpellCheckerSession {
     86     private static final String TAG = SpellCheckerSession.class.getSimpleName();
     87     private static final boolean DBG = false;
     88     /**
     89      * Name under which a SpellChecker service component publishes information about itself.
     90      * This meta-data must reference an XML resource.
     91      **/
     92     public static final String SERVICE_META_DATA = "android.view.textservice.scs";
     93 
     94     private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1;
     95     private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2;
     96 
     97     private final InternalListener mInternalListener;
     98     private final ITextServicesManager mTextServicesManager;
     99     private final SpellCheckerInfo mSpellCheckerInfo;
    100     private final SpellCheckerSessionListener mSpellCheckerSessionListener;
    101     private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
    102 
    103     private final CloseGuard mGuard = CloseGuard.get();
    104 
    105     /** Handler that will execute the main tasks */
    106     private final Handler mHandler = new Handler() {
    107         @Override
    108         public void handleMessage(Message msg) {
    109             switch (msg.what) {
    110                 case MSG_ON_GET_SUGGESTION_MULTIPLE:
    111                     handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj);
    112                     break;
    113                 case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE:
    114                     handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj);
    115                     break;
    116             }
    117         }
    118     };
    119 
    120     /**
    121      * Constructor
    122      * @hide
    123      */
    124     public SpellCheckerSession(
    125             SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) {
    126         if (info == null || listener == null || tsm == null) {
    127             throw new NullPointerException();
    128         }
    129         mSpellCheckerInfo = info;
    130         mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler);
    131         mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl);
    132         mTextServicesManager = tsm;
    133         mSpellCheckerSessionListener = listener;
    134 
    135         mGuard.open("finishSession");
    136     }
    137 
    138     /**
    139      * @return true if the connection to a text service of this session is disconnected and not
    140      * alive.
    141      */
    142     public boolean isSessionDisconnected() {
    143         return mSpellCheckerSessionListenerImpl.isDisconnected();
    144     }
    145 
    146     /**
    147      * Get the spell checker service info this spell checker session has.
    148      * @return SpellCheckerInfo for the specified locale.
    149      */
    150     public SpellCheckerInfo getSpellChecker() {
    151         return mSpellCheckerInfo;
    152     }
    153 
    154     /**
    155      * Cancel pending and running spell check tasks
    156      */
    157     public void cancel() {
    158         mSpellCheckerSessionListenerImpl.cancel();
    159     }
    160 
    161     /**
    162      * Finish this session and allow TextServicesManagerService to disconnect the bound spell
    163      * checker.
    164      */
    165     public void close() {
    166         mGuard.close();
    167         try {
    168             mSpellCheckerSessionListenerImpl.close();
    169             mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
    170         } catch (RemoteException e) {
    171             // do nothing
    172         }
    173     }
    174 
    175     /**
    176      * Get suggestions from the specified sentences
    177      * @param textInfos an array of text metadata for a spell checker
    178      * @param suggestionsLimit the maximum number of suggestions that will be returned
    179      */
    180     public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) {
    181         mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple(
    182                 textInfos, suggestionsLimit);
    183     }
    184 
    185     /**
    186      * Get candidate strings for a substring of the specified text.
    187      * @param textInfo text metadata for a spell checker
    188      * @param suggestionsLimit the maximum number of suggestions that will be returned
    189      * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
    190      */
    191     @Deprecated
    192     public void getSuggestions(TextInfo textInfo, int suggestionsLimit) {
    193         getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false);
    194     }
    195 
    196     /**
    197      * A batch process of getSuggestions
    198      * @param textInfos an array of text metadata for a spell checker
    199      * @param suggestionsLimit the maximum number of suggestions that will be returned
    200      * @param sequentialWords true if textInfos can be treated as sequential words.
    201      * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead
    202      */
    203     @Deprecated
    204     public void getSuggestions(
    205             TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
    206         if (DBG) {
    207             Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId());
    208         }
    209         mSpellCheckerSessionListenerImpl.getSuggestionsMultiple(
    210                 textInfos, suggestionsLimit, sequentialWords);
    211     }
    212 
    213     private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) {
    214         mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos);
    215     }
    216 
    217     private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) {
    218         mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos);
    219     }
    220 
    221     private static final class SpellCheckerSessionListenerImpl
    222             extends ISpellCheckerSessionListener.Stub {
    223         private static final int TASK_CANCEL = 1;
    224         private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2;
    225         private static final int TASK_CLOSE = 3;
    226         private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4;
    227         private static String taskToString(int task) {
    228             switch (task) {
    229                 case TASK_CANCEL:
    230                     return "TASK_CANCEL";
    231                 case TASK_GET_SUGGESTIONS_MULTIPLE:
    232                     return "TASK_GET_SUGGESTIONS_MULTIPLE";
    233                 case TASK_CLOSE:
    234                     return "TASK_CLOSE";
    235                 case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
    236                     return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE";
    237                 default:
    238                     return "Unexpected task=" + task;
    239             }
    240         }
    241 
    242         private final Queue<SpellCheckerParams> mPendingTasks = new LinkedList<>();
    243         private Handler mHandler;
    244 
    245         private static final int STATE_WAIT_CONNECTION = 0;
    246         private static final int STATE_CONNECTED = 1;
    247         private static final int STATE_CLOSED_AFTER_CONNECTION = 2;
    248         private static final int STATE_CLOSED_BEFORE_CONNECTION = 3;
    249         private static String stateToString(int state) {
    250             switch (state) {
    251                 case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION";
    252                 case STATE_CONNECTED: return "STATE_CONNECTED";
    253                 case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION";
    254                 case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION";
    255                 default: return "Unexpected state=" + state;
    256             }
    257         }
    258         private int mState = STATE_WAIT_CONNECTION;
    259 
    260         private ISpellCheckerSession mISpellCheckerSession;
    261         private HandlerThread mThread;
    262         private Handler mAsyncHandler;
    263 
    264         public SpellCheckerSessionListenerImpl(Handler handler) {
    265             mHandler = handler;
    266         }
    267 
    268         private static class SpellCheckerParams {
    269             public final int mWhat;
    270             public final TextInfo[] mTextInfos;
    271             public final int mSuggestionsLimit;
    272             public final boolean mSequentialWords;
    273             public ISpellCheckerSession mSession;
    274             public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit,
    275                     boolean sequentialWords) {
    276                 mWhat = what;
    277                 mTextInfos = textInfos;
    278                 mSuggestionsLimit = suggestionsLimit;
    279                 mSequentialWords = sequentialWords;
    280             }
    281         }
    282 
    283         private void processTask(ISpellCheckerSession session, SpellCheckerParams scp,
    284                 boolean async) {
    285             if (DBG) {
    286                 synchronized (this) {
    287                     Log.d(TAG, "entering processTask:"
    288                             + " session.hashCode()=#" + Integer.toHexString(session.hashCode())
    289                             + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async
    290                             + " mAsyncHandler=" + mAsyncHandler
    291                             + " mState=" + stateToString(mState));
    292                 }
    293             }
    294             if (async || mAsyncHandler == null) {
    295                 switch (scp.mWhat) {
    296                     case TASK_CANCEL:
    297                         try {
    298                             session.onCancel();
    299                         } catch (RemoteException e) {
    300                             Log.e(TAG, "Failed to cancel " + e);
    301                         }
    302                         break;
    303                     case TASK_GET_SUGGESTIONS_MULTIPLE:
    304                         try {
    305                             session.onGetSuggestionsMultiple(scp.mTextInfos,
    306                                     scp.mSuggestionsLimit, scp.mSequentialWords);
    307                         } catch (RemoteException e) {
    308                             Log.e(TAG, "Failed to get suggestions " + e);
    309                         }
    310                         break;
    311                     case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE:
    312                         try {
    313                             session.onGetSentenceSuggestionsMultiple(
    314                                     scp.mTextInfos, scp.mSuggestionsLimit);
    315                         } catch (RemoteException e) {
    316                             Log.e(TAG, "Failed to get suggestions " + e);
    317                         }
    318                         break;
    319                     case TASK_CLOSE:
    320                         try {
    321                             session.onClose();
    322                         } catch (RemoteException e) {
    323                             Log.e(TAG, "Failed to close " + e);
    324                         }
    325                         break;
    326                 }
    327             } else {
    328                 // The interface is to a local object, so need to execute it
    329                 // asynchronously.
    330                 scp.mSession = session;
    331                 mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp));
    332             }
    333 
    334             if (scp.mWhat == TASK_CLOSE) {
    335                 // If we are closing, we want to clean up our state now even
    336                 // if it is pending as an async operation.
    337                 synchronized (this) {
    338                     processCloseLocked();
    339                 }
    340             }
    341         }
    342 
    343         private void processCloseLocked() {
    344             if (DBG) Log.d(TAG, "entering processCloseLocked:"
    345                     + " session" + (mISpellCheckerSession != null ? ".hashCode()=#"
    346                             + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null")
    347                     + " mState=" + stateToString(mState));
    348             mISpellCheckerSession = null;
    349             if (mThread != null) {
    350                 mThread.quit();
    351             }
    352             mHandler = null;
    353             mPendingTasks.clear();
    354             mThread = null;
    355             mAsyncHandler = null;
    356             switch (mState) {
    357                 case STATE_WAIT_CONNECTION:
    358                     mState = STATE_CLOSED_BEFORE_CONNECTION;
    359                     break;
    360                 case STATE_CONNECTED:
    361                     mState = STATE_CLOSED_AFTER_CONNECTION;
    362                     break;
    363                 default:
    364                     Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" +
    365                             stateToString(mState));
    366                     break;
    367             }
    368         }
    369 
    370         public void onServiceConnected(ISpellCheckerSession session) {
    371             synchronized (this) {
    372                 switch (mState) {
    373                     case STATE_WAIT_CONNECTION:
    374                         // OK, go ahead.
    375                         break;
    376                     case STATE_CLOSED_BEFORE_CONNECTION:
    377                         // This is possible, and not an error.  The client no longer is interested
    378                         // in this connection. OK to ignore.
    379                         if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is"
    380                                 + " already closed.");
    381                         return;
    382                     default:
    383                         Log.e(TAG, "ignoring onServiceConnected due to unexpected mState="
    384                                 + stateToString(mState));
    385                         return;
    386                 }
    387                 if (session == null) {
    388                     Log.e(TAG, "ignoring onServiceConnected due to session=null");
    389                     return;
    390                 }
    391                 mISpellCheckerSession = session;
    392                 if (session.asBinder() instanceof Binder && mThread == null) {
    393                     if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected.");
    394                     // If this is a local object, we need to do our own threading
    395                     // to make sure we handle it asynchronously.
    396                     mThread = new HandlerThread("SpellCheckerSession",
    397                             Process.THREAD_PRIORITY_BACKGROUND);
    398                     mThread.start();
    399                     mAsyncHandler = new Handler(mThread.getLooper()) {
    400                         @Override public void handleMessage(Message msg) {
    401                             SpellCheckerParams scp = (SpellCheckerParams)msg.obj;
    402                             processTask(scp.mSession, scp, true);
    403                         }
    404                     };
    405                 }
    406                 mState = STATE_CONNECTED;
    407                 if (DBG) {
    408                     Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#"
    409                             + Integer.toHexString(mISpellCheckerSession.hashCode())
    410                             + " mPendingTasks.size()=" + mPendingTasks.size());
    411                 }
    412                 while (!mPendingTasks.isEmpty()) {
    413                     processTask(session, mPendingTasks.poll(), false);
    414                 }
    415             }
    416         }
    417 
    418         public void cancel() {
    419             processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false));
    420         }
    421 
    422         public void getSuggestionsMultiple(
    423                 TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) {
    424             processOrEnqueueTask(
    425                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos,
    426                             suggestionsLimit, sequentialWords));
    427         }
    428 
    429         public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) {
    430             processOrEnqueueTask(
    431                     new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE,
    432                             textInfos, suggestionsLimit, false));
    433         }
    434 
    435         public void close() {
    436             processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false));
    437         }
    438 
    439         public boolean isDisconnected() {
    440             synchronized (this) {
    441                 return mState != STATE_CONNECTED;
    442             }
    443         }
    444 
    445         private void processOrEnqueueTask(SpellCheckerParams scp) {
    446             ISpellCheckerSession session;
    447             synchronized (this) {
    448                 if (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION
    449                         || mState == STATE_CLOSED_BEFORE_CONNECTION)) {
    450                     // It is OK to call SpellCheckerSession#close() multiple times.
    451                     // Don't output confusing/misleading warning messages.
    452                     return;
    453                 }
    454                 if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) {
    455                     Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState="
    456                             + stateToString(mState)
    457                             + " scp.mWhat=" + taskToString(scp.mWhat));
    458                     return;
    459                 }
    460 
    461                 if (mState == STATE_WAIT_CONNECTION) {
    462                     // If we are still waiting for the connection. Need to pay special attention.
    463                     if (scp.mWhat == TASK_CLOSE) {
    464                         processCloseLocked();
    465                         return;
    466                     }
    467                     // Enqueue the task to task queue.
    468                     SpellCheckerParams closeTask = null;
    469                     if (scp.mWhat == TASK_CANCEL) {
    470                         if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask.");
    471                         while (!mPendingTasks.isEmpty()) {
    472                             final SpellCheckerParams tmp = mPendingTasks.poll();
    473                             if (tmp.mWhat == TASK_CLOSE) {
    474                                 // Only one close task should be processed, while we need to remove
    475                                 // all close tasks from the queue
    476                                 closeTask = tmp;
    477                             }
    478                         }
    479                     }
    480                     mPendingTasks.offer(scp);
    481                     if (closeTask != null) {
    482                         mPendingTasks.offer(closeTask);
    483                     }
    484                     if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the"
    485                             + " connection is not established."
    486                             + " mPendingTasks.size()=" + mPendingTasks.size());
    487                     return;
    488                 }
    489 
    490                 session = mISpellCheckerSession;
    491             }
    492             // session must never be null here.
    493             processTask(session, scp, false);
    494         }
    495 
    496         @Override
    497         public void onGetSuggestions(SuggestionsInfo[] results) {
    498             synchronized (this) {
    499                 if (mHandler != null) {
    500                     mHandler.sendMessage(Message.obtain(mHandler,
    501                             MSG_ON_GET_SUGGESTION_MULTIPLE, results));
    502                 }
    503             }
    504         }
    505 
    506         @Override
    507         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
    508             synchronized (this) {
    509                 if (mHandler != null) {
    510                     mHandler.sendMessage(Message.obtain(mHandler,
    511                             MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results));
    512                 }
    513             }
    514         }
    515     }
    516 
    517     /**
    518      * Callback for getting results from text services
    519      */
    520     public interface SpellCheckerSessionListener {
    521         /**
    522          * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)}
    523          * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
    524          * @param results an array of {@link SuggestionsInfo}s.
    525          * These results are suggestions for {@link TextInfo}s queried by
    526          * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or
    527          * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)}
    528          */
    529         public void onGetSuggestions(SuggestionsInfo[] results);
    530         /**
    531          * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}
    532          * @param results an array of {@link SentenceSuggestionsInfo}s.
    533          * These results are suggestions for {@link TextInfo}s
    534          * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}.
    535          */
    536         public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results);
    537     }
    538 
    539     private static final class InternalListener extends ITextServicesSessionListener.Stub {
    540         private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl;
    541 
    542         public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) {
    543             mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl;
    544         }
    545 
    546         @Override
    547         public void onServiceConnected(ISpellCheckerSession session) {
    548             mParentSpellCheckerSessionListenerImpl.onServiceConnected(session);
    549         }
    550     }
    551 
    552     @Override
    553     protected void finalize() throws Throwable {
    554         try {
    555             // Note that mGuard will be null if the constructor threw.
    556             if (mGuard != null) {
    557                 mGuard.warnIfOpen();
    558                 close();
    559             }
    560         } finally {
    561             super.finalize();
    562         }
    563     }
    564 
    565     /**
    566      * @hide
    567      */
    568     public ITextServicesSessionListener getTextServicesSessionListener() {
    569         return mInternalListener;
    570     }
    571 
    572     /**
    573      * @hide
    574      */
    575     public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
    576         return mSpellCheckerSessionListenerImpl;
    577     }
    578 }
    579