content: /*
   * Copyright (C) 2014 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.app;
  
  import android.annotation.CallbackExecutor;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.Context;
  import android.os.Bundle;
  import android.os.IBinder;
  import android.os.ICancellationSignal;
  import android.os.Looper;
  import android.os.Message;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.os.RemoteException;
  import android.util.ArrayMap;
  import android.util.DebugUtils;
  import android.util.Log;
  
  import com.android.internal.app.IVoiceInteractor;
  import com.android.internal.app.IVoiceInteractorCallback;
  import com.android.internal.app.IVoiceInteractorRequest;
  import com.android.internal.os.HandlerCaller;
  import com.android.internal.os.SomeArgs;
  import com.android.internal.util.Preconditions;
  import com.android.internal.util.function.pooled.PooledLambda;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.lang.ref.WeakReference;
  import java.util.ArrayList;
  import java.util.concurrent.Executor;
  
  /**
   * Interface for an {@link Activity} to interact with the user through voice.  Use
   * {@link android.app.Activity#getVoiceInteractor() Activity.getVoiceInteractor}
   * to retrieve the interface, if the activity is currently involved in a voice interaction.
   *
   * <p>The voice interactor revolves around submitting voice interaction requests to the
   * back-end voice interaction service that is working with the user.  These requests are
   * submitted with {@link #submitRequest}, providing a new instance of a
   * {@link Request} subclass describing the type of operation to perform -- currently the
   * possible requests are {@link ConfirmationRequest} and {@link CommandRequest}.
   *
   * <p>Once a request is submitted, the voice system will process it and eventually deliver
   * the result to the request object.  The application can cancel a pending request at any
   * time.
   *
   * <p>The VoiceInteractor is integrated with Activity's state saving mechanism, so that
   * if an activity is being restarted with retained state, it will retain the current
   * VoiceInteractor and any outstanding requests.  Because of this, you should always use
   * {@link Request#getActivity() Request.getActivity} to get back to the activity of a
   * request, rather than holding on to the activity instance yourself, either explicitly
   * or implicitly through a non-static inner class.
   */
  public final class VoiceInteractor {
      static final String TAG = "VoiceInteractor";
      static final boolean DEBUG = false;
  
      static final Request[] NO_REQUESTS = new Request[0];
  
      /** @hide */
      public static final String KEY_CANCELLATION_SIGNAL = "key_cancellation_signal";
      /** @hide */
      public static final String KEY_KILL_SIGNAL = "key_kill_signal";
  
      @Nullable IVoiceInteractor mInteractor;
  
      @Nullable Context mContext;
      @Nullable Activity mActivity;
      boolean mRetaining;
  
      final HandlerCaller mHandlerCaller;
      final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() {
          @Override
          public void executeMessage(Message msg) {
              SomeArgs args = (SomeArgs)msg.obj;
              Request request;
              boolean complete;
              switch (msg.what) {
                  case MSG_CONFIRMATION_RESULT:
                      request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
                      if (DEBUG) Log.d(TAG, "onConfirmResult: req="
                              + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
                              + " confirmed=" + msg.arg1 + " result=" + args.arg2);
                      if (request != null) {
                          ((ConfirmationRequest)request).onConfirmationResult(msg.arg1 != 0,
                                  (Bundle) args.arg2);
                          request.clear();
                      }
                      break;
                  case MSG_PICK_OPTION_RESULT:
                      complete = msg.arg1 != 0;
                      request = pullRequest((IVoiceInteractorRequest)args.arg1, complete);
                      if (DEBUG) Log.d(TAG, "onPickOptionResult: req="
                              + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
                              + " finished=" + complete + " selection=" + args.arg2
                              + " result=" + args.arg3);
                      if (request != null) {
                          ((PickOptionRequest)request).onPickOptionResult(complete,
                                  (PickOptionRequest.Option[]) args.arg2, (Bundle) args.arg3);
                          if (complete) {
                              request.clear();
                          }
                      }
                      break;
                  case MSG_COMPLETE_VOICE_RESULT:
                      request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
                      if (DEBUG) Log.d(TAG, "onCompleteVoice: req="
                              + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
                              + " result=" + args.arg2);
                      if (request != null) {
                          ((CompleteVoiceRequest)request).onCompleteResult((Bundle) args.arg2);
                          request.clear();
                      }
                      break;
                  case MSG_ABORT_VOICE_RESULT:
                      request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
                      if (DEBUG) Log.d(TAG, "onAbortVoice: req="
                              + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
                              + " result=" + args.arg2);
                      if (request != null) {
                          ((AbortVoiceRequest)request).onAbortResult((Bundle) args.arg2);
                          request.clear();
                      }
                      break;
                  case MSG_COMMAND_RESULT:
                      complete = msg.arg1 != 0;
                      request = pullRequest((IVoiceInteractorRequest)args.arg1, complete);
                      if (DEBUG) Log.d(TAG, "onCommandResult: req="
                              + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
                              + " completed=" + msg.arg1 + " result=" + args.arg2);
                      if (request != null) {
                          ((CommandRequest)request).onCommandResult(msg.arg1 != 0,
                                  (Bundle) args.arg2);
                          if (complete) {
                              request.clear();
                          }
                      }
                      break;
                  case MSG_CANCEL_RESULT:
                      request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
                      if (DEBUG) Log.d(TAG, "onCancelResult: req="
                              + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request);
                      if (request != null) {
                          request.onCancel();
                          request.clear();
                      }
                      break;
              }
          }
      };
  
      final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() {
          @Override
          public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean finished,
                  Bundle result) {
              mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
                      MSG_CONFIRMATION_RESULT, finished ? 1 : 0, request, result));
          }
  
          @Override
          public void deliverPickOptionResult(IVoiceInteractorRequest request,
                  boolean finished, PickOptionRequest.Option[] options, Bundle result) {
              mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOOO(
                      MSG_PICK_OPTION_RESULT, finished ? 1 : 0, request, options, result));
          }
  
          @Override
          public void deliverCompleteVoiceResult(IVoiceInteractorRequest request, Bundle result) {
              mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
                      MSG_COMPLETE_VOICE_RESULT, request, result));
          }
  
          @Override
          public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result) {
              mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
                      MSG_ABORT_VOICE_RESULT, request, result));
          }
  
          @Override
          public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete,
                  Bundle result) {
              mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
                      MSG_COMMAND_RESULT, complete ? 1 : 0, request, result));
          }
  
          @Override
          public void deliverCancel(IVoiceInteractorRequest request) {
              mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
                      MSG_CANCEL_RESULT, request, null));
          }
  
          @Override
          public void destroy() {
              mHandlerCaller.getHandler().sendMessage(PooledLambda.obtainMessage(
                      VoiceInteractor::destroy, VoiceInteractor.this));
          }
      };
  
      final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<>();
      final ArrayMap<Runnable, Executor> mOnDestroyCallbacks = new ArrayMap<>();
  
      static final int MSG_CONFIRMATION_RESULT = 1;
      static final int MSG_PICK_OPTION_RESULT = 2;
      static final int MSG_COMPLETE_VOICE_RESULT = 3;
      static final int MSG_ABORT_VOICE_RESULT = 4;
      static final int MSG_COMMAND_RESULT = 5;
      static final int MSG_CANCEL_RESULT = 6;
  
      /**
       * Base class for voice interaction requests that can be submitted to the interactor.
       * Do not instantiate this directly -- instead, use the appropriate subclass.
       */
      public static abstract class Request {
          IVoiceInteractorRequest mRequestInterface;
          Context mContext;
          Activity mActivity;
          String mName;
  
          Request() {
          }
  
          /**
           * Return the name this request was submitted through
           * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}.
           */
          public String getName() {
              return mName;
          }
  
          /**
           * Cancel this active request.
           */
          public void cancel() {
              if (mRequestInterface == null) {
                  throw new IllegalStateException("Request " + this + " is no longer active");
              }
              try {
                  mRequestInterface.cancel();
              } catch (RemoteException e) {
                  Log.w(TAG, "Voice interactor has died", e);
              }
          }
  
          /**
           * Return the current {@link Context} this request is associated with.  May change
           * if the activity hosting it goes through a configuration change.
           */
          public Context getContext() {
              return mContext;
          }
  
          /**
           * Return the current {@link Activity} this request is associated with.  Will change
           * if the activity is restarted such as through a configuration change.
           */
          public Activity getActivity() {
              return mActivity;
          }
  
          /**
           * Report from voice interaction service: this operation has been canceled, typically
           * as a completion of a previous call to {@link #cancel} or when the user explicitly
           * cancelled.
           */
          public void onCancel() {
          }
  
          /**
           * The request is now attached to an activity, or being re-attached to a new activity
           * after a configuration change.
           */
          public void onAttached(Activity activity) {
          }
  
          /**
           * The request is being detached from an activity.
           */
          public void onDetached() {
          }
  
          @Override
          public String toString() {
              StringBuilder sb = new StringBuilder(128);
              DebugUtils.buildShortClassTag(this, sb);
              sb.append(" ");
              sb.append(getRequestTypeName());
              sb.append(" name=");
              sb.append(mName);
              sb.append('}');
              return sb.toString();
          }
  
          void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
              writer.print(prefix); writer.print("mRequestInterface=");
              writer.println(mRequestInterface.asBinder());
              writer.print(prefix); writer.print("mActivity="); writer.println(mActivity);
              writer.print(prefix); writer.print("mName="); writer.println(mName);
          }
  
          String getRequestTypeName() {
              return "Request";
          }
  
          void clear() {
              mRequestInterface = null;
              mContext = null;
              mActivity = null;
              mName = null;
          }
  
          abstract IVoiceInteractorRequest submit(IVoiceInteractor interactor,
                  String packageName, IVoiceInteractorCallback callback) throws RemoteException;
      }
  
      /**
       * Confirms an operation with the user via the trusted system
       * VoiceInteractionService.  This allows an Activity to complete an unsafe operation that
       * would require the user to touch the screen when voice interaction mode is not enabled.
       * The result of the confirmation will be returned through an asynchronous call to
       * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or
       * {@link #onCancel()} - these methods should be overridden to define the application specific
       *  behavior.
       *
       * <p>In some cases this may be a simple yes / no confirmation or the confirmation could
       * include context information about how the action will be completed
       * (e.g. booking a cab might include details about how long until the cab arrives)
       * so the user can give a confirmation.
       */
      public static class ConfirmationRequest extends Request {
          final Prompt mPrompt;
          final Bundle mExtras;
  
          /**
           * Create a new confirmation request.
           * @param prompt Optional confirmation to speak to the user or null if nothing
           *     should be spoken.
           * @param extras Additional optional information or null.
           */
          public ConfirmationRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
              mPrompt = prompt;
              mExtras = extras;
          }
  
          /**
           * Create a new confirmation request.
           * @param prompt Optional confirmation to speak to the user or null if nothing
           *     should be spoken.
           * @param extras Additional optional information or null.
           * @hide
           */
          public ConfirmationRequest(CharSequence prompt, Bundle extras) {
              mPrompt = (prompt != null ? new Prompt(prompt) : null);
              mExtras = extras;
          }
  
          /**
           * Handle the confirmation result. Override this method to define
           * the behavior when the user confirms or rejects the operation.
           * @param confirmed Whether the user confirmed or rejected the operation.
           * @param result Additional result information or null.
           */
          public void onConfirmationResult(boolean confirmed, Bundle result) {
          }
  
          void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
              super.dump(prefix, fd, writer, args);
              writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
              if (mExtras != null) {
                  writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
              }
          }
  
          String getRequestTypeName() {
              return "Confirmation";
          }
  
          IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
                  IVoiceInteractorCallback callback) throws RemoteException {
              return interactor.startConfirmation(packageName, callback, mPrompt, mExtras);
          }
      }
  
      /**
       * Select a single option from multiple potential options with the user via the trusted system
       * VoiceInteractionService. Typically, the application would present this visually as
       * a list view to allow selecting the option by touch.
       * The result of the confirmation will be returned through an asynchronous call to
       * either {@link #onPickOptionResult} or {@link #onCancel()} - these methods should
       * be overridden to define the application specific behavior.
       */
      public static class PickOptionRequest extends Request {
          final Prompt mPrompt;
          final Option[] mOptions;
          final Bundle mExtras;
  
          /**
           * Represents a single option that the user may select using their voice. The 
           * {@link #getIndex()} method should be used as a unique ID to identify the option
           * when it is returned from the voice interactor.
           */
          public static final class Option implements Parcelable {
              final CharSequence mLabel;
              final int mIndex;
              ArrayList<CharSequence> mSynonyms;
              Bundle mExtras;
  
              /**
               * Creates an option that a user can select with their voice by matching the label
               * or one of several synonyms.
               * @param label The label that will both be matched against what the user speaks
               *     and displayed visually.
               * @hide
               */
              public Option(CharSequence label) {
                  mLabel = label;
                  mIndex = -1;
              }
  
              /**
               * Creates an option that a user can select with their voice by matching the label
               * or one of several synonyms.
               * @param label The label that will both be matched against what the user speaks
               *     and displayed visually.
               * @param index The location of this option within the overall set of options.
               *     Can be used to help identify the option when it is returned from the
               *     voice interactor.
               */
              public Option(CharSequence label, int index) {
                  mLabel = label;
                  mIndex = index;
              }
  
              /**
               * Add a synonym term to the option to indicate an alternative way the content
               * may be matched.
               * @param synonym The synonym that will be matched against what the user speaks,
               *     but not displayed.
               */
              public Option addSynonym(CharSequence synonym) {
                  if (mSynonyms == null) {
                      mSynonyms = new ArrayList<>();
                  }
                  mSynonyms.add(synonym);
                  return this;
              }
  
              public CharSequence getLabel() {
                  return mLabel;
              }
  
              /**
               * Return the index that was supplied in the constructor.
               * If the option was constructed without an index, -1 is returned.
               */
              public int getIndex() {
                  return mIndex;
              }
  
              public int countSynonyms() {
                  return mSynonyms != null ? mSynonyms.size() : 0;
              }
  
              public CharSequence getSynonymAt(int index) {
                  return mSynonyms != null ? mSynonyms.get(index) : null;
              }
  
              /**
               * Set optional extra information associated with this option.  Note that this
               * method takes ownership of the supplied extras Bundle.
               */
              public void setExtras(Bundle extras) {
                  mExtras = extras;
              }
  
              /**
               * Return any optional extras information associated with this option, or null
               * if there is none.  Note that this method returns a reference to the actual
               * extras Bundle in the option, so modifications to it will directly modify the
               * extras in the option.
               */
              public Bundle getExtras() {
                  return mExtras;
              }
  
              Option(Parcel in) {
                  mLabel = in.readCharSequence();
                  mIndex = in.readInt();
                  mSynonyms = in.readCharSequenceList();
                  mExtras = in.readBundle();
              }
  
              @Override
              public int describeContents() {
                  return 0;
              }
  
              @Override
              public void writeToParcel(Parcel dest, int flags) {
                  dest.writeCharSequence(mLabel);
                  dest.writeInt(mIndex);
                  dest.writeCharSequenceList(mSynonyms);
                  dest.writeBundle(mExtras);
              }
  
              public static final @android.annotation.NonNull Parcelable.Creator<Option> CREATOR
                      = new Parcelable.Creator<Option>() {
                  public Option createFromParcel(Parcel in) {
                      return new Option(in);
                  }
  
                  public Option[] newArray(int size) {
                      return new Option[size];
                  }
              };
          };
  
          /**
           * Create a new pick option request.
           * @param prompt Optional question to be asked of the user when the options are
           *     presented or null if nothing should be asked.
           * @param options The set of {@link Option}s the user is selecting from.
           * @param extras Additional optional information or null.
           */
          public PickOptionRequest(@Nullable Prompt prompt, Option[] options,
                  @Nullable Bundle extras) {
              mPrompt = prompt;
              mOptions = options;
              mExtras = extras;
          }
  
          /**
           * Create a new pick option request.
           * @param prompt Optional question to be asked of the user when the options are
           *     presented or null if nothing should be asked.
           * @param options The set of {@link Option}s the user is selecting from.
           * @param extras Additional optional information or null.
           * @hide
           */
          public PickOptionRequest(CharSequence prompt, Option[] options, Bundle extras) {
              mPrompt = (prompt != null ? new Prompt(prompt) : null);
              mOptions = options;
              mExtras = extras;
          }
  
          /**
           * Called when a single option is confirmed or narrowed to one of several options. Override
           * this method to define the behavior when the user selects an option or narrows down the
           * set of options.
           * @param finished True if the voice interaction has finished making a selection, in
           *     which case {@code selections} contains the final result.  If false, this request is
           *     still active and you will continue to get calls on it.
           * @param selections Either a single {@link Option} or one of several {@link Option}s the
           *     user has narrowed the choices down to.
           * @param result Additional optional information.
           */
          public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
          }
  
          void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
              super.dump(prefix, fd, writer, args);
              writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
              if (mOptions != null) {
                  writer.print(prefix); writer.println("Options:");
                  for (int i=0; i<mOptions.length; i++) {
                      Option op = mOptions[i];
                      writer.print(prefix); writer.print("  #"); writer.print(i); writer.println(":");
                      writer.print(prefix); writer.print("    mLabel="); writer.println(op.mLabel);
                      writer.print(prefix); writer.print("    mIndex="); writer.println(op.mIndex);
                      if (op.mSynonyms != null && op.mSynonyms.size() > 0) {
                          writer.print(prefix); writer.println("    Synonyms:");
                          for (int j=0; j<op.mSynonyms.size(); j++) {
                              writer.print(prefix); writer.print("      #"); writer.print(j);
                              writer.print(": "); writer.println(op.mSynonyms.get(j));
                          }
                      }
                      if (op.mExtras != null) {
                          writer.print(prefix); writer.print("    mExtras=");
                          writer.println(op.mExtras);
                      }
                  }
              }
              if (mExtras != null) {
                  writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
              }
          }
  
          String getRequestTypeName() {
              return "PickOption";
          }
  
          IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
                  IVoiceInteractorCallback callback) throws RemoteException {
              return interactor.startPickOption(packageName, callback, mPrompt, mOptions, mExtras);
          }
      }
  
      /**
       * Reports that the current interaction was successfully completed with voice, so the
       * application can report the final status to the user. When the response comes back, the
       * voice system has handled the request and is ready to switch; at that point the
       * application can start a new non-voice activity or finish.  Be sure when starting the new
       * activity to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
       * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
       * interaction task.
       */
      public static class CompleteVoiceRequest extends Request {
          final Prompt mPrompt;
          final Bundle mExtras;
  
          /**
           * Create a new completed voice interaction request.
           * @param prompt Optional message to speak to the user about the completion status of
           *     the task or null if nothing should be spoken.
           * @param extras Additional optional information or null.
           */
          public CompleteVoiceRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
              mPrompt = prompt;
              mExtras = extras;
          }
  
          /**
           * Create a new completed voice interaction request.
           * @param message Optional message to speak to the user about the completion status of
           *     the task or null if nothing should be spoken.
           * @param extras Additional optional information or null.
           * @hide
           */
          public CompleteVoiceRequest(CharSequence message, Bundle extras) {
              mPrompt = (message != null ? new Prompt(message) : null);
              mExtras = extras;
          }
  
          public void onCompleteResult(Bundle result) {
          }
  
          void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
              super.dump(prefix, fd, writer, args);
              writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
              if (mExtras != null) {
                  writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
              }
          }
  
          String getRequestTypeName() {
              return "CompleteVoice";
          }
  
          IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
                  IVoiceInteractorCallback callback) throws RemoteException {
              return interactor.startCompleteVoice(packageName, callback, mPrompt, mExtras);
          }
      }
  
      /**
       * Reports that the current interaction can not be complete with voice, so the
       * application will need to switch to a traditional input UI.  Applications should
       * only use this when they need to completely bail out of the voice interaction
       * and switch to a traditional UI.  When the response comes back, the voice
       * system has handled the request and is ready to switch; at that point the application
       * can start a new non-voice activity.  Be sure when starting the new activity
       * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
       * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
       * interaction task.
       */
      public static class AbortVoiceRequest extends Request {
          final Prompt mPrompt;
          final Bundle mExtras;
  
          /**
           * Create a new voice abort request.
           * @param prompt Optional message to speak to the user indicating why the task could
           *     not be completed by voice or null if nothing should be spoken.
           * @param extras Additional optional information or null.
           */
          public AbortVoiceRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
              mPrompt = prompt;
              mExtras = extras;
          }
  
          /**
           * Create a new voice abort request.
           * @param message Optional message to speak to the user indicating why the task could
           *     not be completed by voice or null if nothing should be spoken.
           * @param extras Additional optional information or null.
           * @hide
           */
          public AbortVoiceRequest(CharSequence message, Bundle extras) {
              mPrompt = (message != null ? new Prompt(message) : null);
              mExtras = extras;
          }
  
          public void onAbortResult(Bundle result) {
          }
  
          void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
              super.dump(prefix, fd, writer, args);
              writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
              if (mExtras != null) {
                  writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
              }
          }
  
          String getRequestTypeName() {
              return "AbortVoice";
          }
  
          IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
                  IVoiceInteractorCallback callback) throws RemoteException {
              return interactor.startAbortVoice(packageName, callback, mPrompt, mExtras);
          }
      }
  
      /**
       * Execute a vendor-specific command using the trusted system VoiceInteractionService.
       * This allows an Activity to request additional information from the user needed to
       * complete an action (e.g. booking a table might have several possible times that the
       * user could select from or an app might need the user to agree to a terms of service).
       * The result of the confirmation will be returned through an asynchronous call to
       * either {@link #onCommandResult(boolean, android.os.Bundle)} or
       * {@link #onCancel()}.
       *
       * <p>The command is a string that describes the generic operation to be performed.
       * The command will determine how the properties in extras are interpreted and the set of
       * available commands is expected to grow over time.  An example might be
       * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of
       * airline check-in.  (This is not an actual working example.)
       */
      public static class CommandRequest extends Request {
          final String mCommand;
          final Bundle mArgs;
  
          /**
           * Create a new generic command request.
           * @param command The desired command to perform.
           * @param args Additional arguments to control execution of the command.
           */
          public CommandRequest(String command, Bundle args) {
              mCommand = command;
              mArgs = args;
          }
  
          /**
           * Results for CommandRequest can be returned in partial chunks.
           * The isCompleted is set to true iff all results have been returned, indicating the
           * CommandRequest has completed.
           */
          public void onCommandResult(boolean isCompleted, Bundle result) {
          }
  
          void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
              super.dump(prefix, fd, writer, args);
              writer.print(prefix); writer.print("mCommand="); writer.println(mCommand);
              if (mArgs != null) {
                  writer.print(prefix); writer.print("mArgs="); writer.println(mArgs);
              }
          }
  
          String getRequestTypeName() {
              return "Command";
          }
  
          IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
                  IVoiceInteractorCallback callback) throws RemoteException {
              return interactor.startCommand(packageName, callback, mCommand, mArgs);
          }
      }
  
      /**
       * A set of voice prompts to use with the voice interaction system to confirm an action, select
       * an option, or do similar operations. Multiple voice prompts may be provided for variety. A
       * visual prompt must be provided, which might not match the spoken version. For example, the
       * confirmation "Are you sure you want to purchase this item?" might use a visual label like
       * "Purchase item".
       */
      public static class Prompt implements Parcelable {
          // Mandatory voice prompt. Must contain at least one item, which must not be null.
          private final CharSequence[] mVoicePrompts;
  
          // Mandatory visual prompt.
          private final CharSequence mVisualPrompt;
  
          /**
           * Constructs a prompt set.
           * @param voicePrompts An array of one or more voice prompts. Must not be empty or null.
           * @param visualPrompt A prompt to display on the screen. Must not be null.
           */
          public Prompt(@NonNull CharSequence[] voicePrompts, @NonNull CharSequence visualPrompt) {
              if (voicePrompts == null) {
                  throw new NullPointerException("voicePrompts must not be null");
              }
              if (voicePrompts.length == 0) {
                  throw new IllegalArgumentException("voicePrompts must not be empty");
              }
              if (visualPrompt == null) {
                  throw new NullPointerException("visualPrompt must not be null");
              }
              this.mVoicePrompts = voicePrompts;
              this.mVisualPrompt = visualPrompt;
          }
  
          /**
           * Constructs a prompt set with single prompt used for all interactions. This is most useful
           * in test apps. Non-trivial apps should prefer the detailed constructor.
           */
          public Prompt(@NonNull CharSequence prompt) {
              this.mVoicePrompts = new CharSequence[] { prompt };
              this.mVisualPrompt = prompt;
          }
  
          /**
           * Returns a prompt to use for voice interactions.
           */
          @NonNull
          public CharSequence getVoicePromptAt(int index) {
              return mVoicePrompts[index];
          }
  
          /**
           * Returns the number of different voice prompts.
           */
          public int countVoicePrompts() {
              return mVoicePrompts.length;
          }
  
          /**
           * Returns the prompt to use for visual display.
           */
          @NonNull
          public CharSequence getVisualPrompt() {
              return mVisualPrompt;
          }
  
          @Override
          public String toString() {
              StringBuilder sb = new StringBuilder(128);
              DebugUtils.buildShortClassTag(this, sb);
              if (mVisualPrompt != null && mVoicePrompts != null && mVoicePrompts.length == 1
                  && mVisualPrompt.equals(mVoicePrompts[0])) {
                  sb.append(" ");
                  sb.append(mVisualPrompt);
              } else {
                  if (mVisualPrompt != null) {
                      sb.append(" visual="); sb.append(mVisualPrompt);
                  }
                  if (mVoicePrompts != null) {
                      sb.append(", voice=");
                      for (int i=0; i<mVoicePrompts.length; i++) {
                          if (i > 0) sb.append(" | ");
                          sb.append(mVoicePrompts[i]);
                      }
                  }
              }
              sb.append('}');
              return sb.toString();
          }
  
          /** Constructor to support Parcelable behavior. */
          Prompt(Parcel in) {
              mVoicePrompts = in.readCharSequenceArray();
              mVisualPrompt = in.readCharSequence();
          }
  
          @Override
          public int describeContents() {
              return 0;
          }
  
          @Override
          public void writeToParcel(Parcel dest, int flags) {
              dest.writeCharSequenceArray(mVoicePrompts);
              dest.writeCharSequence(mVisualPrompt);
          }
  
          public static final @android.annotation.NonNull Creator<Prompt> CREATOR
                  = new Creator<Prompt>() {
              public Prompt createFromParcel(Parcel in) {
                  return new Prompt(in);
              }
  
              public Prompt[] newArray(int size) {
                  return new Prompt[size];
              }
          };
      }
  
      VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity,
              Looper looper) {
          mInteractor = interactor;
          mContext = context;
          mActivity = activity;
          mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true);
          try {
              mInteractor.setKillCallback(new KillCallback(this));
          } catch (RemoteException e) {
              /* ignore */
          }
      }
  
      Request pullRequest(IVoiceInteractorRequest request, boolean complete) {
          synchronized (mActiveRequests) {
              Request req = mActiveRequests.get(request.asBinder());
              if (req != null && complete) {
                  mActiveRequests.remove(request.asBinder());
              }
              return req;
          }
      }
  
      private ArrayList<Request> makeRequestList() {
          final int N = mActiveRequests.size();
          if (N < 1) {
              return null;
          }
          ArrayList<Request> list = new ArrayList<>(N);
          for (int i=0; i<N; i++) {
              list.add(mActiveRequests.valueAt(i));
          }
          return list;
      }
  
      void attachActivity(Activity activity) {
          mRetaining = false;
          if (mActivity == activity) {
              return;
          }
          mContext = activity;
          mActivity = activity;
          ArrayList<Request> reqs = makeRequestList();
          if (reqs != null) {
              for (int i=0; i<reqs.size(); i++) {
                  Request req = reqs.get(i);
                  req.mContext = activity;
                  req.mActivity = activity;
                  req.onAttached(activity);
              }
          }
      }
  
      void retainInstance() {
          mRetaining = true;
      }
  
      void detachActivity() {
          ArrayList<Request> reqs = makeRequestList();
          if (reqs != null) {
              for (int i=0; i<reqs.size(); i++) {
                  Request req = reqs.get(i);
                  req.onDetached();
                  req.mActivity = null;
                  req.mContext = null;
              }
          }
          if (!mRetaining) {
              reqs = makeRequestList();
              if (reqs != null) {
                  for (int i=0; i<reqs.size(); i++) {
                      Request req = reqs.get(i);
                      req.cancel();
                  }
              }
              mActiveRequests.clear();
          }
          mContext = null;
          mActivity = null;
      }
  
      void destroy() {
          final int requestCount = mActiveRequests.size();
          for (int i = requestCount - 1; i >= 0; i--) {
              final Request request = mActiveRequests.valueAt(i);
              mActiveRequests.removeAt(i);
              request.cancel();
          }
  
          final int callbackCount = mOnDestroyCallbacks.size();
          for (int i = callbackCount - 1; i >= 0; i--) {
              final Runnable callback = mOnDestroyCallbacks.keyAt(i);
              final Executor executor = mOnDestroyCallbacks.valueAt(i);
              executor.execute(callback);
              mOnDestroyCallbacks.removeAt(i);
          }
  
          // destroyed now
          mInteractor = null;
          if (mActivity != null) {
              mActivity.setVoiceInteractor(null);
          }
      }
  
      public boolean submitRequest(Request request) {
          return submitRequest(request, null);
      }
  
      /**
       * Submit a new {@link Request} to the voice interaction service.  The request must be
       * one of the available subclasses -- {@link ConfirmationRequest}, {@link PickOptionRequest},
       * {@link CompleteVoiceRequest}, {@link AbortVoiceRequest}, or {@link CommandRequest}.
       *
       * @param request The desired request to submit.
       * @param name An optional name for this request, or null. This can be used later with
       * {@link #getActiveRequests} and {@link #getActiveRequest} to find the request.
       *
       * @return Returns true of the request was successfully submitted, else false.
       */
      public boolean submitRequest(Request request, String name) {
          if (isDestroyed()) {
              Log.w(TAG, "Cannot interact with a destroyed voice interactor");
              return false;
          }
          try {
              if (request.mRequestInterface != null) {
                  throw new IllegalStateException("Given " + request + " is already active");
              }
              IVoiceInteractorRequest ireq = request.submit(mInteractor,
                      mContext.getOpPackageName(), mCallback);
              request.mRequestInterface = ireq;
              request.mContext = mContext;
              request.mActivity = mActivity;
              request.mName = name;
              synchronized (mActiveRequests) {
                  mActiveRequests.put(ireq.asBinder(), request);
              }
              return true;
          } catch (RemoteException e) {
              Log.w(TAG, "Remove voice interactor service died", e);
              return false;
          }
      }
  
      /**
       * Return all currently active requests.
       */
      public Request[] getActiveRequests() {
          if (isDestroyed()) {
              Log.w(TAG, "Cannot interact with a destroyed voice interactor");
              return null;
          }
          synchronized (mActiveRequests) {
              final int N = mActiveRequests.size();
              if (N <= 0) {
                  return NO_REQUESTS;
              }
              Request[] requests = new Request[N];
              for (int i=0; i<N; i++) {
                  requests[i] = mActiveRequests.valueAt(i);
              }
              return requests;
          }
      }
  
      /**
       * Return any currently active request that was submitted with the given name.
       *
       * @param name The name used to submit the request, as per
       * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}.
       * @return Returns the active request with that name, or null if there was none.
       */
      public Request getActiveRequest(String name) {
          if (isDestroyed()) {
              Log.w(TAG, "Cannot interact with a destroyed voice interactor");
              return null;
          }
          synchronized (mActiveRequests) {
              final int N = mActiveRequests.size();
              for (int i=0; i<N; i++) {
                  Request req = mActiveRequests.valueAt(i);
                  if (name == req.getName() || (name != null && name.equals(req.getName()))) {
                      return req;
                  }
              }
          }
          return null;
      }
  
      /**
       * Queries the supported commands available from the VoiceInteractionService.
       * The command is a string that describes the generic operation to be performed.
       * An example might be "org.example.commands.PICK_DATE" to ask the user to pick
       * a date.  (Note: This is not an actual working example.)
       *
       * @param commands The array of commands to query for support.
       * @return Array of booleans indicating whether each command is supported or not.
       */
      public boolean[] supportsCommands(String[] commands) {
          if (isDestroyed()) {
              Log.w(TAG, "Cannot interact with a destroyed voice interactor");
              return new boolean[commands.length];
          }
          try {
              boolean[] res = mInteractor.supportsCommands(mContext.getOpPackageName(), commands);
              if (DEBUG) Log.d(TAG, "supportsCommands: cmds=" + commands + " res=" + res);
              return res;
          } catch (RemoteException e) {
              throw new RuntimeException("Voice interactor has died", e);
          }
      }
  
      /**
       * @return whether the voice interactor is destroyed. You should not interact
       * with a destroyed voice interactor.
       */
      public boolean isDestroyed() {
          return mInteractor == null;
      }
  
      /**
       * Registers a callback to be called when the VoiceInteractor is destroyed.
       *
       * @param executor Executor on which to run the callback.
       * @param callback The callback to run.
       * @return whether the callback was registered.
       */
      public boolean registerOnDestroyedCallback(@NonNull @CallbackExecutor Executor executor,
              @NonNull Runnable callback) {
          Preconditions.checkNotNull(executor);
          Preconditions.checkNotNull(callback);
          if (isDestroyed()) {
              Log.w(TAG, "Cannot interact with a destroyed voice interactor");
              return false;
          }
          mOnDestroyCallbacks.put(callback, executor);
          return true;
      }
  
      /**
       * Unregisters a previously registered onDestroy callback
       *
       * @param callback The callback to remove.
       * @return whether the callback was unregistered.
       */
      public boolean unregisterOnDestroyedCallback(@NonNull Runnable callback) {
          Preconditions.checkNotNull(callback);
          if (isDestroyed()) {
              Log.w(TAG, "Cannot interact with a destroyed voice interactor");
              return false;
          }
          return mOnDestroyCallbacks.remove(callback) != null;
      }
  
      /**
       * Notifies the assist framework that the direct actions supported by the app changed.
       */
      public void notifyDirectActionsChanged() {
          if (isDestroyed()) {
              Log.w(TAG, "Cannot interact with a destroyed voice interactor");
              return;
          }
          try {
              mInteractor.notifyDirectActionsChanged(mActivity.getTaskId(),
                      mActivity.getAssistToken());
          } catch (RemoteException e) {
              Log.w(TAG, "Voice interactor has died", e);
          }
      }
  
      void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
          String innerPrefix = prefix + "    ";
          if (mActiveRequests.size() > 0) {
              writer.print(prefix); writer.println("Active voice requests:");
              for (int i=0; i<mActiveRequests.size(); i++) {
                  Request req = mActiveRequests.valueAt(i);
                  writer.print(prefix); writer.print("  #"); writer.print(i);
                  writer.print(": ");
                  writer.println(req);
                  req.dump(innerPrefix, fd, writer, args);
              }
          }
          writer.print(prefix); writer.println("VoiceInteractor misc state:");
          writer.print(prefix); writer.print("  mInteractor=");
          writer.println(mInteractor.asBinder());
          writer.print(prefix); writer.print("  mActivity="); writer.println(mActivity);
      }
  
      private static final class KillCallback extends ICancellationSignal.Stub {
          private final WeakReference<VoiceInteractor> mInteractor;
  
          KillCallback(VoiceInteractor interactor) {
              mInteractor= new WeakReference<>(interactor);
          }
  
          @Override
          public void cancel() {
              final VoiceInteractor voiceInteractor = mInteractor.get();
              if (voiceInteractor != null) {
                  voiceInteractor.mHandlerCaller.getHandler().sendMessage(PooledLambda
                          .obtainMessage(VoiceInteractor::destroy, voiceInteractor));
              }
          }
      }
  }
  