/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view.textservice; import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.util.Log; import com.android.internal.textservice.ISpellCheckerSession; import com.android.internal.textservice.ISpellCheckerSessionListener; import com.android.internal.textservice.ITextServicesManager; import com.android.internal.textservice.ITextServicesSessionListener; import java.util.LinkedList; import java.util.Queue; /** * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService. * * * *

Applications

* *

In most cases, applications that are using the standard * {@link android.widget.TextView} or its subclasses will have little they need * to do to work well with spell checker services. The main things you need to * be aware of are:

* * * *

For the rare people amongst us writing client applications that use the spell checker service * directly, you will need to use {@link #getSuggestions(TextInfo, int)} or * {@link #getSuggestions(TextInfo[], int, boolean)} for obtaining results from the spell checker * service by yourself.

* *

Security

* *

There are a lot of security issues associated with spell checkers, * since they could monitor all the text being sent to them * through, for instance, {@link android.widget.TextView}. * The Android spell checker framework also allows * arbitrary third party spell checkers, so care must be taken to restrict their * selection and interactions.

* *

Here are some key points about the security architecture behind the * spell checker framework:

* * * */ public class SpellCheckerSession { private static final String TAG = SpellCheckerSession.class.getSimpleName(); private static final boolean DBG = false; /** * Name under which a SpellChecker service component publishes information about itself. * This meta-data must reference an XML resource. **/ public static final String SERVICE_META_DATA = "android.view.textservice.scs"; private static final int MSG_ON_GET_SUGGESTION_MULTIPLE = 1; private static final int MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE = 2; private final InternalListener mInternalListener; private final ITextServicesManager mTextServicesManager; private final SpellCheckerInfo mSpellCheckerInfo; private final SpellCheckerSessionListener mSpellCheckerSessionListener; private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl; private boolean mIsUsed; /** Handler that will execute the main tasks */ private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_ON_GET_SUGGESTION_MULTIPLE: handleOnGetSuggestionsMultiple((SuggestionsInfo[]) msg.obj); break; case MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE: handleOnGetSentenceSuggestionsMultiple((SentenceSuggestionsInfo[]) msg.obj); break; } } }; /** * Constructor * @hide */ public SpellCheckerSession( SpellCheckerInfo info, ITextServicesManager tsm, SpellCheckerSessionListener listener) { if (info == null || listener == null || tsm == null) { throw new NullPointerException(); } mSpellCheckerInfo = info; mSpellCheckerSessionListenerImpl = new SpellCheckerSessionListenerImpl(mHandler); mInternalListener = new InternalListener(mSpellCheckerSessionListenerImpl); mTextServicesManager = tsm; mIsUsed = true; mSpellCheckerSessionListener = listener; } /** * @return true if the connection to a text service of this session is disconnected and not * alive. */ public boolean isSessionDisconnected() { return mSpellCheckerSessionListenerImpl.isDisconnected(); } /** * Get the spell checker service info this spell checker session has. * @return SpellCheckerInfo for the specified locale. */ public SpellCheckerInfo getSpellChecker() { return mSpellCheckerInfo; } /** * Cancel pending and running spell check tasks */ public void cancel() { mSpellCheckerSessionListenerImpl.cancel(); } /** * Finish this session and allow TextServicesManagerService to disconnect the bound spell * checker. */ public void close() { mIsUsed = false; try { mSpellCheckerSessionListenerImpl.close(); mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl); } catch (RemoteException e) { // do nothing } } /** * Get suggestions from the specified sentences * @param textInfos an array of text metadata for a spell checker * @param suggestionsLimit the maximum number of suggestions that will be returned */ public void getSentenceSuggestions(TextInfo[] textInfos, int suggestionsLimit) { mSpellCheckerSessionListenerImpl.getSentenceSuggestionsMultiple( textInfos, suggestionsLimit); } /** * Get candidate strings for a substring of the specified text. * @param textInfo text metadata for a spell checker * @param suggestionsLimit the maximum number of suggestions that will be returned * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead */ @Deprecated public void getSuggestions(TextInfo textInfo, int suggestionsLimit) { getSuggestions(new TextInfo[] {textInfo}, suggestionsLimit, false); } /** * A batch process of getSuggestions * @param textInfos an array of text metadata for a spell checker * @param suggestionsLimit the maximum number of suggestions that will be returned * @param sequentialWords true if textInfos can be treated as sequential words. * @deprecated use {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} instead */ @Deprecated public void getSuggestions( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { if (DBG) { Log.w(TAG, "getSuggestions from " + mSpellCheckerInfo.getId()); } mSpellCheckerSessionListenerImpl.getSuggestionsMultiple( textInfos, suggestionsLimit, sequentialWords); } private void handleOnGetSuggestionsMultiple(SuggestionsInfo[] suggestionInfos) { mSpellCheckerSessionListener.onGetSuggestions(suggestionInfos); } private void handleOnGetSentenceSuggestionsMultiple(SentenceSuggestionsInfo[] suggestionInfos) { mSpellCheckerSessionListener.onGetSentenceSuggestions(suggestionInfos); } private static final class SpellCheckerSessionListenerImpl extends ISpellCheckerSessionListener.Stub { private static final int TASK_CANCEL = 1; private static final int TASK_GET_SUGGESTIONS_MULTIPLE = 2; private static final int TASK_CLOSE = 3; private static final int TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE = 4; private static String taskToString(int task) { switch (task) { case TASK_CANCEL: return "TASK_CANCEL"; case TASK_GET_SUGGESTIONS_MULTIPLE: return "TASK_GET_SUGGESTIONS_MULTIPLE"; case TASK_CLOSE: return "TASK_CLOSE"; case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: return "TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE"; default: return "Unexpected task=" + task; } } private final Queue mPendingTasks = new LinkedList<>(); private Handler mHandler; private static final int STATE_WAIT_CONNECTION = 0; private static final int STATE_CONNECTED = 1; private static final int STATE_CLOSED_AFTER_CONNECTION = 2; private static final int STATE_CLOSED_BEFORE_CONNECTION = 3; private static String stateToString(int state) { switch (state) { case STATE_WAIT_CONNECTION: return "STATE_WAIT_CONNECTION"; case STATE_CONNECTED: return "STATE_CONNECTED"; case STATE_CLOSED_AFTER_CONNECTION: return "STATE_CLOSED_AFTER_CONNECTION"; case STATE_CLOSED_BEFORE_CONNECTION: return "STATE_CLOSED_BEFORE_CONNECTION"; default: return "Unexpected state=" + state; } } private int mState = STATE_WAIT_CONNECTION; private ISpellCheckerSession mISpellCheckerSession; private HandlerThread mThread; private Handler mAsyncHandler; public SpellCheckerSessionListenerImpl(Handler handler) { mHandler = handler; } private static class SpellCheckerParams { public final int mWhat; public final TextInfo[] mTextInfos; public final int mSuggestionsLimit; public final boolean mSequentialWords; public ISpellCheckerSession mSession; public SpellCheckerParams(int what, TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { mWhat = what; mTextInfos = textInfos; mSuggestionsLimit = suggestionsLimit; mSequentialWords = sequentialWords; } } private void processTask(ISpellCheckerSession session, SpellCheckerParams scp, boolean async) { if (DBG) { synchronized (this) { Log.d(TAG, "entering processTask:" + " session.hashCode()=#" + Integer.toHexString(session.hashCode()) + " scp.mWhat=" + taskToString(scp.mWhat) + " async=" + async + " mAsyncHandler=" + mAsyncHandler + " mState=" + stateToString(mState)); } } if (async || mAsyncHandler == null) { switch (scp.mWhat) { case TASK_CANCEL: try { session.onCancel(); } catch (RemoteException e) { Log.e(TAG, "Failed to cancel " + e); } break; case TASK_GET_SUGGESTIONS_MULTIPLE: try { session.onGetSuggestionsMultiple(scp.mTextInfos, scp.mSuggestionsLimit, scp.mSequentialWords); } catch (RemoteException e) { Log.e(TAG, "Failed to get suggestions " + e); } break; case TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE: try { session.onGetSentenceSuggestionsMultiple( scp.mTextInfos, scp.mSuggestionsLimit); } catch (RemoteException e) { Log.e(TAG, "Failed to get suggestions " + e); } break; case TASK_CLOSE: try { session.onClose(); } catch (RemoteException e) { Log.e(TAG, "Failed to close " + e); } break; } } else { // The interface is to a local object, so need to execute it // asynchronously. scp.mSession = session; mAsyncHandler.sendMessage(Message.obtain(mAsyncHandler, 1, scp)); } if (scp.mWhat == TASK_CLOSE) { // If we are closing, we want to clean up our state now even // if it is pending as an async operation. synchronized (this) { processCloseLocked(); } } } private void processCloseLocked() { if (DBG) Log.d(TAG, "entering processCloseLocked:" + " session" + (mISpellCheckerSession != null ? ".hashCode()=#" + Integer.toHexString(mISpellCheckerSession.hashCode()) : "=null") + " mState=" + stateToString(mState)); mISpellCheckerSession = null; if (mThread != null) { mThread.quit(); } mHandler = null; mPendingTasks.clear(); mThread = null; mAsyncHandler = null; switch (mState) { case STATE_WAIT_CONNECTION: mState = STATE_CLOSED_BEFORE_CONNECTION; break; case STATE_CONNECTED: mState = STATE_CLOSED_AFTER_CONNECTION; break; default: Log.e(TAG, "processCloseLocked is called unexpectedly. mState=" + stateToString(mState)); break; } } public void onServiceConnected(ISpellCheckerSession session) { synchronized (this) { switch (mState) { case STATE_WAIT_CONNECTION: // OK, go ahead. break; case STATE_CLOSED_BEFORE_CONNECTION: // This is possible, and not an error. The client no longer is interested // in this connection. OK to ignore. if (DBG) Log.i(TAG, "ignoring onServiceConnected since the session is" + " already closed."); return; default: Log.e(TAG, "ignoring onServiceConnected due to unexpected mState=" + stateToString(mState)); return; } if (session == null) { Log.e(TAG, "ignoring onServiceConnected due to session=null"); return; } mISpellCheckerSession = session; if (session.asBinder() instanceof Binder && mThread == null) { if (DBG) Log.d(TAG, "starting HandlerThread in onServiceConnected."); // If this is a local object, we need to do our own threading // to make sure we handle it asynchronously. mThread = new HandlerThread("SpellCheckerSession", Process.THREAD_PRIORITY_BACKGROUND); mThread.start(); mAsyncHandler = new Handler(mThread.getLooper()) { @Override public void handleMessage(Message msg) { SpellCheckerParams scp = (SpellCheckerParams)msg.obj; processTask(scp.mSession, scp, true); } }; } mState = STATE_CONNECTED; if (DBG) { Log.d(TAG, "processed onServiceConnected: mISpellCheckerSession.hashCode()=#" + Integer.toHexString(mISpellCheckerSession.hashCode()) + " mPendingTasks.size()=" + mPendingTasks.size()); } while (!mPendingTasks.isEmpty()) { processTask(session, mPendingTasks.poll(), false); } } } public void cancel() { processOrEnqueueTask(new SpellCheckerParams(TASK_CANCEL, null, 0, false)); } public void getSuggestionsMultiple( TextInfo[] textInfos, int suggestionsLimit, boolean sequentialWords) { processOrEnqueueTask( new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE, textInfos, suggestionsLimit, sequentialWords)); } public void getSentenceSuggestionsMultiple(TextInfo[] textInfos, int suggestionsLimit) { processOrEnqueueTask( new SpellCheckerParams(TASK_GET_SUGGESTIONS_MULTIPLE_FOR_SENTENCE, textInfos, suggestionsLimit, false)); } public void close() { processOrEnqueueTask(new SpellCheckerParams(TASK_CLOSE, null, 0, false)); } public boolean isDisconnected() { synchronized (this) { return mState != STATE_CONNECTED; } } private void processOrEnqueueTask(SpellCheckerParams scp) { ISpellCheckerSession session; synchronized (this) { if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) { Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState=" + taskToString(scp.mWhat) + " scp.mWhat=" + taskToString(scp.mWhat)); return; } if (mState == STATE_WAIT_CONNECTION) { // If we are still waiting for the connection. Need to pay special attention. if (scp.mWhat == TASK_CLOSE) { processCloseLocked(); return; } // Enqueue the task to task queue. SpellCheckerParams closeTask = null; if (scp.mWhat == TASK_CANCEL) { if (DBG) Log.d(TAG, "canceling pending tasks in processOrEnqueueTask."); while (!mPendingTasks.isEmpty()) { final SpellCheckerParams tmp = mPendingTasks.poll(); if (tmp.mWhat == TASK_CLOSE) { // Only one close task should be processed, while we need to remove // all close tasks from the queue closeTask = tmp; } } } mPendingTasks.offer(scp); if (closeTask != null) { mPendingTasks.offer(closeTask); } if (DBG) Log.d(TAG, "queueing tasks in processOrEnqueueTask since the" + " connection is not established." + " mPendingTasks.size()=" + mPendingTasks.size()); return; } session = mISpellCheckerSession; } // session must never be null here. processTask(session, scp, false); } @Override public void onGetSuggestions(SuggestionsInfo[] results) { synchronized (this) { if (mHandler != null) { mHandler.sendMessage(Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE, results)); } } } @Override public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { synchronized (this) { if (mHandler != null) { mHandler.sendMessage(Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); } } } } /** * Callback for getting results from text services */ public interface SpellCheckerSessionListener { /** * Callback for {@link SpellCheckerSession#getSuggestions(TextInfo, int)} * and {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} * @param results an array of {@link SuggestionsInfo}s. * These results are suggestions for {@link TextInfo}s queried by * {@link SpellCheckerSession#getSuggestions(TextInfo, int)} or * {@link SpellCheckerSession#getSuggestions(TextInfo[], int, boolean)} */ public void onGetSuggestions(SuggestionsInfo[] results); /** * Callback for {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)} * @param results an array of {@link SentenceSuggestionsInfo}s. * These results are suggestions for {@link TextInfo}s * queried by {@link SpellCheckerSession#getSentenceSuggestions(TextInfo[], int)}. */ public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results); } private static final class InternalListener extends ITextServicesSessionListener.Stub { private final SpellCheckerSessionListenerImpl mParentSpellCheckerSessionListenerImpl; public InternalListener(SpellCheckerSessionListenerImpl spellCheckerSessionListenerImpl) { mParentSpellCheckerSessionListenerImpl = spellCheckerSessionListenerImpl; } @Override public void onServiceConnected(ISpellCheckerSession session) { mParentSpellCheckerSessionListenerImpl.onServiceConnected(session); } } @Override protected void finalize() throws Throwable { super.finalize(); if (mIsUsed) { Log.e(TAG, "SpellCheckerSession was not finished properly." + "You should call finishSession() when you finished to use a spell checker."); close(); } } /** * @hide */ public ITextServicesSessionListener getTextServicesSessionListener() { return mInternalListener; } /** * @hide */ public ISpellCheckerSessionListener getSpellCheckerSessionListener() { return mSpellCheckerSessionListenerImpl; } }