content: /*
   * 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.annotation.UnsupportedAppUsage;
  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.ITextServicesSessionListener;
  
  import dalvik.system.CloseGuard;
  
  import java.util.LinkedList;
  import java.util.Queue;
  
  /**
   * The SpellCheckerSession interface provides the per client functionality of SpellCheckerService.
   *
   *
   * <a name="Applications"></a>
   * <h3>Applications</h3>
   *
   * <p>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:</p>
   *
   * <ul>
   * <li> Properly set the {@link android.R.attr#inputType} in your editable
   * text views, so that the spell checker will have enough context to help the
   * user in editing text in them.
   * </ul>
   *
   * <p>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.</p>
   *
   * <h3>Security</h3>
   *
   * <p>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.</p>
   *
   * <p>Here are some key points about the security architecture behind the
   * spell checker framework:</p>
   *
   * <ul>
   * <li>Only the system is allowed to directly access a spell checker framework's
   * {@link android.service.textservice.SpellCheckerService} interface, via the
   * {@link android.Manifest.permission#BIND_TEXT_SERVICE} permission.  This is
   * enforced in the system by not binding to a spell checker service that does
   * not require this permission.
   *
   * <li>The user must explicitly enable a new spell checker in settings before
   * they can be enabled, to confirm with the system that they know about it
   * and want to make it available for use.
   * </ul>
   *
   */
  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 TextServicesManager mTextServicesManager;
      private final SpellCheckerInfo mSpellCheckerInfo;
      @UnsupportedAppUsage
      private final SpellCheckerSessionListener mSpellCheckerSessionListener;
      private final SpellCheckerSessionListenerImpl mSpellCheckerSessionListenerImpl;
  
      private final CloseGuard mGuard = CloseGuard.get();
  
      /** 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, TextServicesManager 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;
          mSpellCheckerSessionListener = listener;
  
          mGuard.open("finishSession");
      }
  
      /**
       * @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() {
          mGuard.close();
          mSpellCheckerSessionListenerImpl.close();
          mTextServicesManager.finishSpellCheckerService(mSpellCheckerSessionListenerImpl);
      }
  
      /**
       * 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<SpellCheckerParams> 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 (scp.mWhat == TASK_CLOSE && (mState == STATE_CLOSED_AFTER_CONNECTION
                          || mState == STATE_CLOSED_BEFORE_CONNECTION)) {
                      // It is OK to call SpellCheckerSession#close() multiple times.
                      // Don't output confusing/misleading warning messages.
                      return;
                  }
                  if (mState != STATE_WAIT_CONNECTION && mState != STATE_CONNECTED) {
                      Log.e(TAG, "ignoring processOrEnqueueTask due to unexpected mState="
                              + stateToString(mState)
                              + " 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 {
          try {
              // Note that mGuard will be null if the constructor threw.
              if (mGuard != null) {
                  mGuard.warnIfOpen();
                  close();
              }
          } finally {
              super.finalize();
          }
      }
  
      /**
       * @hide
       */
      public ITextServicesSessionListener getTextServicesSessionListener() {
          return mInternalListener;
      }
  
      /**
       * @hide
       */
      public ISpellCheckerSessionListener getSpellCheckerSessionListener() {
          return mSpellCheckerSessionListenerImpl;
      }
  }
  