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