| /* |
| * 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.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| 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 java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| |
| /** |
| * 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]; |
| |
| final IVoiceInteractor mInteractor; |
| |
| Context mContext; |
| 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) throws RemoteException { |
| mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO( |
| MSG_CANCEL_RESULT, request, null)); |
| } |
| }; |
| |
| final ArrayMap<IBinder, Request> mActiveRequests = 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 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 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); |
| } |
| |
| 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; |
| } |
| |
| 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) { |
| 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() { |
| 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) { |
| 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) { |
| 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); |
| } |
| } |
| |
| 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); |
| } |
| } |