blob: c262ec2bf00994f983fc4d83dccfa7aedd816037 [file] [log] [blame]
/*
* 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.Objects;
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) {
Objects.requireNonNull(executor);
Objects.requireNonNull(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) {
Objects.requireNonNull(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));
}
}
}
}