1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app;
18 
19 import android.annotation.CallbackExecutor;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.os.Bundle;
24 import android.os.IBinder;
25 import android.os.ICancellationSignal;
26 import android.os.Looper;
27 import android.os.Message;
28 import android.os.Parcel;
29 import android.os.Parcelable;
30 import android.os.RemoteException;
31 import android.util.ArrayMap;
32 import android.util.DebugUtils;
33 import android.util.Log;
34 
35 import com.android.internal.app.IVoiceInteractor;
36 import com.android.internal.app.IVoiceInteractorCallback;
37 import com.android.internal.app.IVoiceInteractorRequest;
38 import com.android.internal.os.HandlerCaller;
39 import com.android.internal.os.SomeArgs;
40 import com.android.internal.util.Preconditions;
41 import com.android.internal.util.function.pooled.PooledLambda;
42 
43 import java.io.FileDescriptor;
44 import java.io.PrintWriter;
45 import java.lang.ref.WeakReference;
46 import java.util.ArrayList;
47 import java.util.Objects;
48 import java.util.concurrent.Executor;
49 
50 /**
51  * Interface for an {@link Activity} to interact with the user through voice.  Use
52  * {@link android.app.Activity#getVoiceInteractor() Activity.getVoiceInteractor}
53  * to retrieve the interface, if the activity is currently involved in a voice interaction.
54  *
55  * <p>The voice interactor revolves around submitting voice interaction requests to the
56  * back-end voice interaction service that is working with the user.  These requests are
57  * submitted with {@link #submitRequest}, providing a new instance of a
58  * {@link Request} subclass describing the type of operation to perform -- currently the
59  * possible requests are {@link ConfirmationRequest} and {@link CommandRequest}.
60  *
61  * <p>Once a request is submitted, the voice system will process it and eventually deliver
62  * the result to the request object.  The application can cancel a pending request at any
63  * time.
64  *
65  * <p>The VoiceInteractor is integrated with Activity's state saving mechanism, so that
66  * if an activity is being restarted with retained state, it will retain the current
67  * VoiceInteractor and any outstanding requests.  Because of this, you should always use
68  * {@link Request#getActivity() Request.getActivity} to get back to the activity of a
69  * request, rather than holding on to the activity instance yourself, either explicitly
70  * or implicitly through a non-static inner class.
71  */
72 public final class VoiceInteractor {
73     static final String TAG = "VoiceInteractor";
74     static final boolean DEBUG = false;
75 
76     static final Request[] NO_REQUESTS = new Request[0];
77 
78     /** @hide */
79     public static final String KEY_CANCELLATION_SIGNAL = "key_cancellation_signal";
80     /** @hide */
81     public static final String KEY_KILL_SIGNAL = "key_kill_signal";
82 
83     @Nullable IVoiceInteractor mInteractor;
84 
85     @Nullable Context mContext;
86     @Nullable Activity mActivity;
87     boolean mRetaining;
88 
89     final HandlerCaller mHandlerCaller;
90     final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() {
91         @Override
92         public void executeMessage(Message msg) {
93             SomeArgs args = (SomeArgs)msg.obj;
94             Request request;
95             boolean complete;
96             switch (msg.what) {
97                 case MSG_CONFIRMATION_RESULT:
98                     request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
99                     if (DEBUG) Log.d(TAG, "onConfirmResult: req="
100                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
101                             + " confirmed=" + msg.arg1 + " result=" + args.arg2);
102                     if (request != null) {
103                         ((ConfirmationRequest)request).onConfirmationResult(msg.arg1 != 0,
104                                 (Bundle) args.arg2);
105                         request.clear();
106                     }
107                     break;
108                 case MSG_PICK_OPTION_RESULT:
109                     complete = msg.arg1 != 0;
110                     request = pullRequest((IVoiceInteractorRequest)args.arg1, complete);
111                     if (DEBUG) Log.d(TAG, "onPickOptionResult: req="
112                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
113                             + " finished=" + complete + " selection=" + args.arg2
114                             + " result=" + args.arg3);
115                     if (request != null) {
116                         ((PickOptionRequest)request).onPickOptionResult(complete,
117                                 (PickOptionRequest.Option[]) args.arg2, (Bundle) args.arg3);
118                         if (complete) {
119                             request.clear();
120                         }
121                     }
122                     break;
123                 case MSG_COMPLETE_VOICE_RESULT:
124                     request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
125                     if (DEBUG) Log.d(TAG, "onCompleteVoice: req="
126                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
127                             + " result=" + args.arg2);
128                     if (request != null) {
129                         ((CompleteVoiceRequest)request).onCompleteResult((Bundle) args.arg2);
130                         request.clear();
131                     }
132                     break;
133                 case MSG_ABORT_VOICE_RESULT:
134                     request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
135                     if (DEBUG) Log.d(TAG, "onAbortVoice: req="
136                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
137                             + " result=" + args.arg2);
138                     if (request != null) {
139                         ((AbortVoiceRequest)request).onAbortResult((Bundle) args.arg2);
140                         request.clear();
141                     }
142                     break;
143                 case MSG_COMMAND_RESULT:
144                     complete = msg.arg1 != 0;
145                     request = pullRequest((IVoiceInteractorRequest)args.arg1, complete);
146                     if (DEBUG) Log.d(TAG, "onCommandResult: req="
147                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
148                             + " completed=" + msg.arg1 + " result=" + args.arg2);
149                     if (request != null) {
150                         ((CommandRequest)request).onCommandResult(msg.arg1 != 0,
151                                 (Bundle) args.arg2);
152                         if (complete) {
153                             request.clear();
154                         }
155                     }
156                     break;
157                 case MSG_CANCEL_RESULT:
158                     request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
159                     if (DEBUG) Log.d(TAG, "onCancelResult: req="
160                             + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request);
161                     if (request != null) {
162                         request.onCancel();
163                         request.clear();
164                     }
165                     break;
166             }
167         }
168     };
169 
170     final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() {
171         @Override
172         public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean finished,
173                 Bundle result) {
174             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
175                     MSG_CONFIRMATION_RESULT, finished ? 1 : 0, request, result));
176         }
177 
178         @Override
179         public void deliverPickOptionResult(IVoiceInteractorRequest request,
180                 boolean finished, PickOptionRequest.Option[] options, Bundle result) {
181             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOOO(
182                     MSG_PICK_OPTION_RESULT, finished ? 1 : 0, request, options, result));
183         }
184 
185         @Override
186         public void deliverCompleteVoiceResult(IVoiceInteractorRequest request, Bundle result) {
187             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
188                     MSG_COMPLETE_VOICE_RESULT, request, result));
189         }
190 
191         @Override
192         public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result) {
193             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
194                     MSG_ABORT_VOICE_RESULT, request, result));
195         }
196 
197         @Override
198         public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete,
199                 Bundle result) {
200             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
201                     MSG_COMMAND_RESULT, complete ? 1 : 0, request, result));
202         }
203 
204         @Override
205         public void deliverCancel(IVoiceInteractorRequest request) {
206             mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
207                     MSG_CANCEL_RESULT, request, null));
208         }
209 
210         @Override
211         public void destroy() {
212             mHandlerCaller.getHandler().sendMessage(PooledLambda.obtainMessage(
213                     VoiceInteractor::destroy, VoiceInteractor.this));
214         }
215     };
216 
217     final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<>();
218     final ArrayMap<Runnable, Executor> mOnDestroyCallbacks = new ArrayMap<>();
219 
220     static final int MSG_CONFIRMATION_RESULT = 1;
221     static final int MSG_PICK_OPTION_RESULT = 2;
222     static final int MSG_COMPLETE_VOICE_RESULT = 3;
223     static final int MSG_ABORT_VOICE_RESULT = 4;
224     static final int MSG_COMMAND_RESULT = 5;
225     static final int MSG_CANCEL_RESULT = 6;
226 
227     /**
228      * Base class for voice interaction requests that can be submitted to the interactor.
229      * Do not instantiate this directly -- instead, use the appropriate subclass.
230      */
231     public static abstract class Request {
232         IVoiceInteractorRequest mRequestInterface;
233         Context mContext;
234         Activity mActivity;
235         String mName;
236 
Request()237         Request() {
238         }
239 
240         /**
241          * Return the name this request was submitted through
242          * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}.
243          */
getName()244         public String getName() {
245             return mName;
246         }
247 
248         /**
249          * Cancel this active request.
250          */
cancel()251         public void cancel() {
252             if (mRequestInterface == null) {
253                 throw new IllegalStateException("Request " + this + " is no longer active");
254             }
255             try {
256                 mRequestInterface.cancel();
257             } catch (RemoteException e) {
258                 Log.w(TAG, "Voice interactor has died", e);
259             }
260         }
261 
262         /**
263          * Return the current {@link Context} this request is associated with.  May change
264          * if the activity hosting it goes through a configuration change.
265          */
getContext()266         public Context getContext() {
267             return mContext;
268         }
269 
270         /**
271          * Return the current {@link Activity} this request is associated with.  Will change
272          * if the activity is restarted such as through a configuration change.
273          */
getActivity()274         public Activity getActivity() {
275             return mActivity;
276         }
277 
278         /**
279          * Report from voice interaction service: this operation has been canceled, typically
280          * as a completion of a previous call to {@link #cancel} or when the user explicitly
281          * cancelled.
282          */
onCancel()283         public void onCancel() {
284         }
285 
286         /**
287          * The request is now attached to an activity, or being re-attached to a new activity
288          * after a configuration change.
289          */
onAttached(Activity activity)290         public void onAttached(Activity activity) {
291         }
292 
293         /**
294          * The request is being detached from an activity.
295          */
onDetached()296         public void onDetached() {
297         }
298 
299         @Override
toString()300         public String toString() {
301             StringBuilder sb = new StringBuilder(128);
302             DebugUtils.buildShortClassTag(this, sb);
303             sb.append(" ");
304             sb.append(getRequestTypeName());
305             sb.append(" name=");
306             sb.append(mName);
307             sb.append('}');
308             return sb.toString();
309         }
310 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)311         void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
312             writer.print(prefix); writer.print("mRequestInterface=");
313             writer.println(mRequestInterface.asBinder());
314             writer.print(prefix); writer.print("mActivity="); writer.println(mActivity);
315             writer.print(prefix); writer.print("mName="); writer.println(mName);
316         }
317 
getRequestTypeName()318         String getRequestTypeName() {
319             return "Request";
320         }
321 
clear()322         void clear() {
323             mRequestInterface = null;
324             mContext = null;
325             mActivity = null;
326             mName = null;
327         }
328 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)329         abstract IVoiceInteractorRequest submit(IVoiceInteractor interactor,
330                 String packageName, IVoiceInteractorCallback callback) throws RemoteException;
331     }
332 
333     /**
334      * Confirms an operation with the user via the trusted system
335      * VoiceInteractionService.  This allows an Activity to complete an unsafe operation that
336      * would require the user to touch the screen when voice interaction mode is not enabled.
337      * The result of the confirmation will be returned through an asynchronous call to
338      * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or
339      * {@link #onCancel()} - these methods should be overridden to define the application specific
340      *  behavior.
341      *
342      * <p>In some cases this may be a simple yes / no confirmation or the confirmation could
343      * include context information about how the action will be completed
344      * (e.g. booking a cab might include details about how long until the cab arrives)
345      * so the user can give a confirmation.
346      */
347     public static class ConfirmationRequest extends Request {
348         final Prompt mPrompt;
349         final Bundle mExtras;
350 
351         /**
352          * Create a new confirmation request.
353          * @param prompt Optional confirmation to speak to the user or null if nothing
354          *     should be spoken.
355          * @param extras Additional optional information or null.
356          */
ConfirmationRequest(@ullable Prompt prompt, @Nullable Bundle extras)357         public ConfirmationRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
358             mPrompt = prompt;
359             mExtras = extras;
360         }
361 
362         /**
363          * Create a new confirmation request.
364          * @param prompt Optional confirmation to speak to the user or null if nothing
365          *     should be spoken.
366          * @param extras Additional optional information or null.
367          * @hide
368          */
ConfirmationRequest(CharSequence prompt, Bundle extras)369         public ConfirmationRequest(CharSequence prompt, Bundle extras) {
370             mPrompt = (prompt != null ? new Prompt(prompt) : null);
371             mExtras = extras;
372         }
373 
374         /**
375          * Handle the confirmation result. Override this method to define
376          * the behavior when the user confirms or rejects the operation.
377          * @param confirmed Whether the user confirmed or rejected the operation.
378          * @param result Additional result information or null.
379          */
onConfirmationResult(boolean confirmed, Bundle result)380         public void onConfirmationResult(boolean confirmed, Bundle result) {
381         }
382 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)383         void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
384             super.dump(prefix, fd, writer, args);
385             writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
386             if (mExtras != null) {
387                 writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
388             }
389         }
390 
getRequestTypeName()391         String getRequestTypeName() {
392             return "Confirmation";
393         }
394 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)395         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
396                 IVoiceInteractorCallback callback) throws RemoteException {
397             return interactor.startConfirmation(packageName, callback, mPrompt, mExtras);
398         }
399     }
400 
401     /**
402      * Select a single option from multiple potential options with the user via the trusted system
403      * VoiceInteractionService. Typically, the application would present this visually as
404      * a list view to allow selecting the option by touch.
405      * The result of the confirmation will be returned through an asynchronous call to
406      * either {@link #onPickOptionResult} or {@link #onCancel()} - these methods should
407      * be overridden to define the application specific behavior.
408      */
409     public static class PickOptionRequest extends Request {
410         final Prompt mPrompt;
411         final Option[] mOptions;
412         final Bundle mExtras;
413 
414         /**
415          * Represents a single option that the user may select using their voice. The
416          * {@link #getIndex()} method should be used as a unique ID to identify the option
417          * when it is returned from the voice interactor.
418          */
419         public static final class Option implements Parcelable {
420             final CharSequence mLabel;
421             final int mIndex;
422             ArrayList<CharSequence> mSynonyms;
423             Bundle mExtras;
424 
425             /**
426              * Creates an option that a user can select with their voice by matching the label
427              * or one of several synonyms.
428              * @param label The label that will both be matched against what the user speaks
429              *     and displayed visually.
430              * @hide
431              */
Option(CharSequence label)432             public Option(CharSequence label) {
433                 mLabel = label;
434                 mIndex = -1;
435             }
436 
437             /**
438              * Creates an option that a user can select with their voice by matching the label
439              * or one of several synonyms.
440              * @param label The label that will both be matched against what the user speaks
441              *     and displayed visually.
442              * @param index The location of this option within the overall set of options.
443              *     Can be used to help identify the option when it is returned from the
444              *     voice interactor.
445              */
Option(CharSequence label, int index)446             public Option(CharSequence label, int index) {
447                 mLabel = label;
448                 mIndex = index;
449             }
450 
451             /**
452              * Add a synonym term to the option to indicate an alternative way the content
453              * may be matched.
454              * @param synonym The synonym that will be matched against what the user speaks,
455              *     but not displayed.
456              */
addSynonym(CharSequence synonym)457             public Option addSynonym(CharSequence synonym) {
458                 if (mSynonyms == null) {
459                     mSynonyms = new ArrayList<>();
460                 }
461                 mSynonyms.add(synonym);
462                 return this;
463             }
464 
getLabel()465             public CharSequence getLabel() {
466                 return mLabel;
467             }
468 
469             /**
470              * Return the index that was supplied in the constructor.
471              * If the option was constructed without an index, -1 is returned.
472              */
getIndex()473             public int getIndex() {
474                 return mIndex;
475             }
476 
countSynonyms()477             public int countSynonyms() {
478                 return mSynonyms != null ? mSynonyms.size() : 0;
479             }
480 
getSynonymAt(int index)481             public CharSequence getSynonymAt(int index) {
482                 return mSynonyms != null ? mSynonyms.get(index) : null;
483             }
484 
485             /**
486              * Set optional extra information associated with this option.  Note that this
487              * method takes ownership of the supplied extras Bundle.
488              */
setExtras(Bundle extras)489             public void setExtras(Bundle extras) {
490                 mExtras = extras;
491             }
492 
493             /**
494              * Return any optional extras information associated with this option, or null
495              * if there is none.  Note that this method returns a reference to the actual
496              * extras Bundle in the option, so modifications to it will directly modify the
497              * extras in the option.
498              */
getExtras()499             public Bundle getExtras() {
500                 return mExtras;
501             }
502 
Option(Parcel in)503             Option(Parcel in) {
504                 mLabel = in.readCharSequence();
505                 mIndex = in.readInt();
506                 mSynonyms = in.readCharSequenceList();
507                 mExtras = in.readBundle();
508             }
509 
510             @Override
describeContents()511             public int describeContents() {
512                 return 0;
513             }
514 
515             @Override
writeToParcel(Parcel dest, int flags)516             public void writeToParcel(Parcel dest, int flags) {
517                 dest.writeCharSequence(mLabel);
518                 dest.writeInt(mIndex);
519                 dest.writeCharSequenceList(mSynonyms);
520                 dest.writeBundle(mExtras);
521             }
522 
523             public static final @android.annotation.NonNull Parcelable.Creator<Option> CREATOR
524                     = new Parcelable.Creator<Option>() {
525                 public Option createFromParcel(Parcel in) {
526                     return new Option(in);
527                 }
528 
529                 public Option[] newArray(int size) {
530                     return new Option[size];
531                 }
532             };
533         };
534 
535         /**
536          * Create a new pick option request.
537          * @param prompt Optional question to be asked of the user when the options are
538          *     presented or null if nothing should be asked.
539          * @param options The set of {@link Option}s the user is selecting from.
540          * @param extras Additional optional information or null.
541          */
PickOptionRequest(@ullable Prompt prompt, Option[] options, @Nullable Bundle extras)542         public PickOptionRequest(@Nullable Prompt prompt, Option[] options,
543                 @Nullable Bundle extras) {
544             mPrompt = prompt;
545             mOptions = options;
546             mExtras = extras;
547         }
548 
549         /**
550          * Create a new pick option request.
551          * @param prompt Optional question to be asked of the user when the options are
552          *     presented or null if nothing should be asked.
553          * @param options The set of {@link Option}s the user is selecting from.
554          * @param extras Additional optional information or null.
555          * @hide
556          */
PickOptionRequest(CharSequence prompt, Option[] options, Bundle extras)557         public PickOptionRequest(CharSequence prompt, Option[] options, Bundle extras) {
558             mPrompt = (prompt != null ? new Prompt(prompt) : null);
559             mOptions = options;
560             mExtras = extras;
561         }
562 
563         /**
564          * Called when a single option is confirmed or narrowed to one of several options. Override
565          * this method to define the behavior when the user selects an option or narrows down the
566          * set of options.
567          * @param finished True if the voice interaction has finished making a selection, in
568          *     which case {@code selections} contains the final result.  If false, this request is
569          *     still active and you will continue to get calls on it.
570          * @param selections Either a single {@link Option} or one of several {@link Option}s the
571          *     user has narrowed the choices down to.
572          * @param result Additional optional information.
573          */
onPickOptionResult(boolean finished, Option[] selections, Bundle result)574         public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
575         }
576 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)577         void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
578             super.dump(prefix, fd, writer, args);
579             writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
580             if (mOptions != null) {
581                 writer.print(prefix); writer.println("Options:");
582                 for (int i=0; i<mOptions.length; i++) {
583                     Option op = mOptions[i];
584                     writer.print(prefix); writer.print("  #"); writer.print(i); writer.println(":");
585                     writer.print(prefix); writer.print("    mLabel="); writer.println(op.mLabel);
586                     writer.print(prefix); writer.print("    mIndex="); writer.println(op.mIndex);
587                     if (op.mSynonyms != null && op.mSynonyms.size() > 0) {
588                         writer.print(prefix); writer.println("    Synonyms:");
589                         for (int j=0; j<op.mSynonyms.size(); j++) {
590                             writer.print(prefix); writer.print("      #"); writer.print(j);
591                             writer.print(": "); writer.println(op.mSynonyms.get(j));
592                         }
593                     }
594                     if (op.mExtras != null) {
595                         writer.print(prefix); writer.print("    mExtras=");
596                         writer.println(op.mExtras);
597                     }
598                 }
599             }
600             if (mExtras != null) {
601                 writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
602             }
603         }
604 
getRequestTypeName()605         String getRequestTypeName() {
606             return "PickOption";
607         }
608 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)609         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
610                 IVoiceInteractorCallback callback) throws RemoteException {
611             return interactor.startPickOption(packageName, callback, mPrompt, mOptions, mExtras);
612         }
613     }
614 
615     /**
616      * Reports that the current interaction was successfully completed with voice, so the
617      * application can report the final status to the user. When the response comes back, the
618      * voice system has handled the request and is ready to switch; at that point the
619      * application can start a new non-voice activity or finish.  Be sure when starting the new
620      * activity to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
621      * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
622      * interaction task.
623      */
624     public static class CompleteVoiceRequest extends Request {
625         final Prompt mPrompt;
626         final Bundle mExtras;
627 
628         /**
629          * Create a new completed voice interaction request.
630          * @param prompt Optional message to speak to the user about the completion status of
631          *     the task or null if nothing should be spoken.
632          * @param extras Additional optional information or null.
633          */
CompleteVoiceRequest(@ullable Prompt prompt, @Nullable Bundle extras)634         public CompleteVoiceRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
635             mPrompt = prompt;
636             mExtras = extras;
637         }
638 
639         /**
640          * Create a new completed voice interaction request.
641          * @param message Optional message to speak to the user about the completion status of
642          *     the task or null if nothing should be spoken.
643          * @param extras Additional optional information or null.
644          * @hide
645          */
CompleteVoiceRequest(CharSequence message, Bundle extras)646         public CompleteVoiceRequest(CharSequence message, Bundle extras) {
647             mPrompt = (message != null ? new Prompt(message) : null);
648             mExtras = extras;
649         }
650 
onCompleteResult(Bundle result)651         public void onCompleteResult(Bundle result) {
652         }
653 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)654         void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
655             super.dump(prefix, fd, writer, args);
656             writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
657             if (mExtras != null) {
658                 writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
659             }
660         }
661 
getRequestTypeName()662         String getRequestTypeName() {
663             return "CompleteVoice";
664         }
665 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)666         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
667                 IVoiceInteractorCallback callback) throws RemoteException {
668             return interactor.startCompleteVoice(packageName, callback, mPrompt, mExtras);
669         }
670     }
671 
672     /**
673      * Reports that the current interaction can not be complete with voice, so the
674      * application will need to switch to a traditional input UI.  Applications should
675      * only use this when they need to completely bail out of the voice interaction
676      * and switch to a traditional UI.  When the response comes back, the voice
677      * system has handled the request and is ready to switch; at that point the application
678      * can start a new non-voice activity.  Be sure when starting the new activity
679      * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
680      * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
681      * interaction task.
682      */
683     public static class AbortVoiceRequest extends Request {
684         final Prompt mPrompt;
685         final Bundle mExtras;
686 
687         /**
688          * Create a new voice abort request.
689          * @param prompt Optional message to speak to the user indicating why the task could
690          *     not be completed by voice or null if nothing should be spoken.
691          * @param extras Additional optional information or null.
692          */
AbortVoiceRequest(@ullable Prompt prompt, @Nullable Bundle extras)693         public AbortVoiceRequest(@Nullable Prompt prompt, @Nullable Bundle extras) {
694             mPrompt = prompt;
695             mExtras = extras;
696         }
697 
698         /**
699          * Create a new voice abort request.
700          * @param message Optional message to speak to the user indicating why the task could
701          *     not be completed by voice or null if nothing should be spoken.
702          * @param extras Additional optional information or null.
703          * @hide
704          */
AbortVoiceRequest(CharSequence message, Bundle extras)705         public AbortVoiceRequest(CharSequence message, Bundle extras) {
706             mPrompt = (message != null ? new Prompt(message) : null);
707             mExtras = extras;
708         }
709 
onAbortResult(Bundle result)710         public void onAbortResult(Bundle result) {
711         }
712 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)713         void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
714             super.dump(prefix, fd, writer, args);
715             writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt);
716             if (mExtras != null) {
717                 writer.print(prefix); writer.print("mExtras="); writer.println(mExtras);
718             }
719         }
720 
getRequestTypeName()721         String getRequestTypeName() {
722             return "AbortVoice";
723         }
724 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)725         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
726                 IVoiceInteractorCallback callback) throws RemoteException {
727             return interactor.startAbortVoice(packageName, callback, mPrompt, mExtras);
728         }
729     }
730 
731     /**
732      * Execute a vendor-specific command using the trusted system VoiceInteractionService.
733      * This allows an Activity to request additional information from the user needed to
734      * complete an action (e.g. booking a table might have several possible times that the
735      * user could select from or an app might need the user to agree to a terms of service).
736      * The result of the confirmation will be returned through an asynchronous call to
737      * either {@link #onCommandResult(boolean, android.os.Bundle)} or
738      * {@link #onCancel()}.
739      *
740      * <p>The command is a string that describes the generic operation to be performed.
741      * The command will determine how the properties in extras are interpreted and the set of
742      * available commands is expected to grow over time.  An example might be
743      * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of
744      * airline check-in.  (This is not an actual working example.)
745      */
746     public static class CommandRequest extends Request {
747         final String mCommand;
748         final Bundle mArgs;
749 
750         /**
751          * Create a new generic command request.
752          * @param command The desired command to perform.
753          * @param args Additional arguments to control execution of the command.
754          */
CommandRequest(String command, Bundle args)755         public CommandRequest(String command, Bundle args) {
756             mCommand = command;
757             mArgs = args;
758         }
759 
760         /**
761          * Results for CommandRequest can be returned in partial chunks.
762          * The isCompleted is set to true iff all results have been returned, indicating the
763          * CommandRequest has completed.
764          */
onCommandResult(boolean isCompleted, Bundle result)765         public void onCommandResult(boolean isCompleted, Bundle result) {
766         }
767 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)768         void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
769             super.dump(prefix, fd, writer, args);
770             writer.print(prefix); writer.print("mCommand="); writer.println(mCommand);
771             if (mArgs != null) {
772                 writer.print(prefix); writer.print("mArgs="); writer.println(mArgs);
773             }
774         }
775 
getRequestTypeName()776         String getRequestTypeName() {
777             return "Command";
778         }
779 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)780         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
781                 IVoiceInteractorCallback callback) throws RemoteException {
782             return interactor.startCommand(packageName, callback, mCommand, mArgs);
783         }
784     }
785 
786     /**
787      * A set of voice prompts to use with the voice interaction system to confirm an action, select
788      * an option, or do similar operations. Multiple voice prompts may be provided for variety. A
789      * visual prompt must be provided, which might not match the spoken version. For example, the
790      * confirmation "Are you sure you want to purchase this item?" might use a visual label like
791      * "Purchase item".
792      */
793     public static class Prompt implements Parcelable {
794         // Mandatory voice prompt. Must contain at least one item, which must not be null.
795         private final CharSequence[] mVoicePrompts;
796 
797         // Mandatory visual prompt.
798         private final CharSequence mVisualPrompt;
799 
800         /**
801          * Constructs a prompt set.
802          * @param voicePrompts An array of one or more voice prompts. Must not be empty or null.
803          * @param visualPrompt A prompt to display on the screen. Must not be null.
804          */
Prompt(@onNull CharSequence[] voicePrompts, @NonNull CharSequence visualPrompt)805         public Prompt(@NonNull CharSequence[] voicePrompts, @NonNull CharSequence visualPrompt) {
806             if (voicePrompts == null) {
807                 throw new NullPointerException("voicePrompts must not be null");
808             }
809             if (voicePrompts.length == 0) {
810                 throw new IllegalArgumentException("voicePrompts must not be empty");
811             }
812             if (visualPrompt == null) {
813                 throw new NullPointerException("visualPrompt must not be null");
814             }
815             this.mVoicePrompts = voicePrompts;
816             this.mVisualPrompt = visualPrompt;
817         }
818 
819         /**
820          * Constructs a prompt set with single prompt used for all interactions. This is most useful
821          * in test apps. Non-trivial apps should prefer the detailed constructor.
822          */
Prompt(@onNull CharSequence prompt)823         public Prompt(@NonNull CharSequence prompt) {
824             this.mVoicePrompts = new CharSequence[] { prompt };
825             this.mVisualPrompt = prompt;
826         }
827 
828         /**
829          * Returns a prompt to use for voice interactions.
830          */
831         @NonNull
getVoicePromptAt(int index)832         public CharSequence getVoicePromptAt(int index) {
833             return mVoicePrompts[index];
834         }
835 
836         /**
837          * Returns the number of different voice prompts.
838          */
countVoicePrompts()839         public int countVoicePrompts() {
840             return mVoicePrompts.length;
841         }
842 
843         /**
844          * Returns the prompt to use for visual display.
845          */
846         @NonNull
getVisualPrompt()847         public CharSequence getVisualPrompt() {
848             return mVisualPrompt;
849         }
850 
851         @Override
toString()852         public String toString() {
853             StringBuilder sb = new StringBuilder(128);
854             DebugUtils.buildShortClassTag(this, sb);
855             if (mVisualPrompt != null && mVoicePrompts != null && mVoicePrompts.length == 1
856                 && mVisualPrompt.equals(mVoicePrompts[0])) {
857                 sb.append(" ");
858                 sb.append(mVisualPrompt);
859             } else {
860                 if (mVisualPrompt != null) {
861                     sb.append(" visual="); sb.append(mVisualPrompt);
862                 }
863                 if (mVoicePrompts != null) {
864                     sb.append(", voice=");
865                     for (int i=0; i<mVoicePrompts.length; i++) {
866                         if (i > 0) sb.append(" | ");
867                         sb.append(mVoicePrompts[i]);
868                     }
869                 }
870             }
871             sb.append('}');
872             return sb.toString();
873         }
874 
875         /** Constructor to support Parcelable behavior. */
Prompt(Parcel in)876         Prompt(Parcel in) {
877             mVoicePrompts = in.readCharSequenceArray();
878             mVisualPrompt = in.readCharSequence();
879         }
880 
881         @Override
describeContents()882         public int describeContents() {
883             return 0;
884         }
885 
886         @Override
writeToParcel(Parcel dest, int flags)887         public void writeToParcel(Parcel dest, int flags) {
888             dest.writeCharSequenceArray(mVoicePrompts);
889             dest.writeCharSequence(mVisualPrompt);
890         }
891 
892         public static final @android.annotation.NonNull Creator<Prompt> CREATOR
893                 = new Creator<Prompt>() {
894             public Prompt createFromParcel(Parcel in) {
895                 return new Prompt(in);
896             }
897 
898             public Prompt[] newArray(int size) {
899                 return new Prompt[size];
900             }
901         };
902     }
903 
VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity, Looper looper)904     VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity,
905             Looper looper) {
906         mInteractor = interactor;
907         mContext = context;
908         mActivity = activity;
909         mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true);
910         try {
911             mInteractor.setKillCallback(new KillCallback(this));
912         } catch (RemoteException e) {
913             /* ignore */
914         }
915     }
916 
pullRequest(IVoiceInteractorRequest request, boolean complete)917     Request pullRequest(IVoiceInteractorRequest request, boolean complete) {
918         synchronized (mActiveRequests) {
919             Request req = mActiveRequests.get(request.asBinder());
920             if (req != null && complete) {
921                 mActiveRequests.remove(request.asBinder());
922             }
923             return req;
924         }
925     }
926 
makeRequestList()927     private ArrayList<Request> makeRequestList() {
928         final int N = mActiveRequests.size();
929         if (N < 1) {
930             return null;
931         }
932         ArrayList<Request> list = new ArrayList<>(N);
933         for (int i=0; i<N; i++) {
934             list.add(mActiveRequests.valueAt(i));
935         }
936         return list;
937     }
938 
attachActivity(Activity activity)939     void attachActivity(Activity activity) {
940         mRetaining = false;
941         if (mActivity == activity) {
942             return;
943         }
944         mContext = activity;
945         mActivity = activity;
946         ArrayList<Request> reqs = makeRequestList();
947         if (reqs != null) {
948             for (int i=0; i<reqs.size(); i++) {
949                 Request req = reqs.get(i);
950                 req.mContext = activity;
951                 req.mActivity = activity;
952                 req.onAttached(activity);
953             }
954         }
955     }
956 
retainInstance()957     void retainInstance() {
958         mRetaining = true;
959     }
960 
detachActivity()961     void detachActivity() {
962         ArrayList<Request> reqs = makeRequestList();
963         if (reqs != null) {
964             for (int i=0; i<reqs.size(); i++) {
965                 Request req = reqs.get(i);
966                 req.onDetached();
967                 req.mActivity = null;
968                 req.mContext = null;
969             }
970         }
971         if (!mRetaining) {
972             reqs = makeRequestList();
973             if (reqs != null) {
974                 for (int i=0; i<reqs.size(); i++) {
975                     Request req = reqs.get(i);
976                     req.cancel();
977                 }
978             }
979             mActiveRequests.clear();
980         }
981         mContext = null;
982         mActivity = null;
983     }
984 
destroy()985     void destroy() {
986         final int requestCount = mActiveRequests.size();
987         for (int i = requestCount - 1; i >= 0; i--) {
988             final Request request = mActiveRequests.valueAt(i);
989             mActiveRequests.removeAt(i);
990             request.cancel();
991         }
992 
993         final int callbackCount = mOnDestroyCallbacks.size();
994         for (int i = callbackCount - 1; i >= 0; i--) {
995             final Runnable callback = mOnDestroyCallbacks.keyAt(i);
996             final Executor executor = mOnDestroyCallbacks.valueAt(i);
997             executor.execute(callback);
998             mOnDestroyCallbacks.removeAt(i);
999         }
1000 
1001         // destroyed now
1002         mInteractor = null;
1003         if (mActivity != null) {
1004             mActivity.setVoiceInteractor(null);
1005         }
1006     }
1007 
submitRequest(Request request)1008     public boolean submitRequest(Request request) {
1009         return submitRequest(request, null);
1010     }
1011 
1012     /**
1013      * Submit a new {@link Request} to the voice interaction service.  The request must be
1014      * one of the available subclasses -- {@link ConfirmationRequest}, {@link PickOptionRequest},
1015      * {@link CompleteVoiceRequest}, {@link AbortVoiceRequest}, or {@link CommandRequest}.
1016      *
1017      * @param request The desired request to submit.
1018      * @param name An optional name for this request, or null. This can be used later with
1019      * {@link #getActiveRequests} and {@link #getActiveRequest} to find the request.
1020      *
1021      * @return Returns true of the request was successfully submitted, else false.
1022      */
submitRequest(Request request, String name)1023     public boolean submitRequest(Request request, String name) {
1024         if (isDestroyed()) {
1025             Log.w(TAG, "Cannot interact with a destroyed voice interactor");
1026             return false;
1027         }
1028         try {
1029             if (request.mRequestInterface != null) {
1030                 throw new IllegalStateException("Given " + request + " is already active");
1031             }
1032             IVoiceInteractorRequest ireq = request.submit(mInteractor,
1033                     mContext.getOpPackageName(), mCallback);
1034             request.mRequestInterface = ireq;
1035             request.mContext = mContext;
1036             request.mActivity = mActivity;
1037             request.mName = name;
1038             synchronized (mActiveRequests) {
1039                 mActiveRequests.put(ireq.asBinder(), request);
1040             }
1041             return true;
1042         } catch (RemoteException e) {
1043             Log.w(TAG, "Remove voice interactor service died", e);
1044             return false;
1045         }
1046     }
1047 
1048     /**
1049      * Return all currently active requests.
1050      */
getActiveRequests()1051     public Request[] getActiveRequests() {
1052         if (isDestroyed()) {
1053             Log.w(TAG, "Cannot interact with a destroyed voice interactor");
1054             return null;
1055         }
1056         synchronized (mActiveRequests) {
1057             final int N = mActiveRequests.size();
1058             if (N <= 0) {
1059                 return NO_REQUESTS;
1060             }
1061             Request[] requests = new Request[N];
1062             for (int i=0; i<N; i++) {
1063                 requests[i] = mActiveRequests.valueAt(i);
1064             }
1065             return requests;
1066         }
1067     }
1068 
1069     /**
1070      * Return any currently active request that was submitted with the given name.
1071      *
1072      * @param name The name used to submit the request, as per
1073      * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}.
1074      * @return Returns the active request with that name, or null if there was none.
1075      */
getActiveRequest(String name)1076     public Request getActiveRequest(String name) {
1077         if (isDestroyed()) {
1078             Log.w(TAG, "Cannot interact with a destroyed voice interactor");
1079             return null;
1080         }
1081         synchronized (mActiveRequests) {
1082             final int N = mActiveRequests.size();
1083             for (int i=0; i<N; i++) {
1084                 Request req = mActiveRequests.valueAt(i);
1085                 if (name == req.getName() || (name != null && name.equals(req.getName()))) {
1086                     return req;
1087                 }
1088             }
1089         }
1090         return null;
1091     }
1092 
1093     /**
1094      * Queries the supported commands available from the VoiceInteractionService.
1095      * The command is a string that describes the generic operation to be performed.
1096      * An example might be "org.example.commands.PICK_DATE" to ask the user to pick
1097      * a date.  (Note: This is not an actual working example.)
1098      *
1099      * @param commands The array of commands to query for support.
1100      * @return Array of booleans indicating whether each command is supported or not.
1101      */
supportsCommands(String[] commands)1102     public boolean[] supportsCommands(String[] commands) {
1103         if (isDestroyed()) {
1104             Log.w(TAG, "Cannot interact with a destroyed voice interactor");
1105             return new boolean[commands.length];
1106         }
1107         try {
1108             boolean[] res = mInteractor.supportsCommands(mContext.getOpPackageName(), commands);
1109             if (DEBUG) Log.d(TAG, "supportsCommands: cmds=" + commands + " res=" + res);
1110             return res;
1111         } catch (RemoteException e) {
1112             throw new RuntimeException("Voice interactor has died", e);
1113         }
1114     }
1115 
1116     /**
1117      * @return whether the voice interactor is destroyed. You should not interact
1118      * with a destroyed voice interactor.
1119      */
isDestroyed()1120     public boolean isDestroyed() {
1121         return mInteractor == null;
1122     }
1123 
1124     /**
1125      * Registers a callback to be called when the VoiceInteractor is destroyed.
1126      *
1127      * @param executor Executor on which to run the callback.
1128      * @param callback The callback to run.
1129      * @return whether the callback was registered.
1130      */
registerOnDestroyedCallback(@onNull @allbackExecutor Executor executor, @NonNull Runnable callback)1131     public boolean registerOnDestroyedCallback(@NonNull @CallbackExecutor Executor executor,
1132             @NonNull Runnable callback) {
1133         Objects.requireNonNull(executor);
1134         Objects.requireNonNull(callback);
1135         if (isDestroyed()) {
1136             Log.w(TAG, "Cannot interact with a destroyed voice interactor");
1137             return false;
1138         }
1139         mOnDestroyCallbacks.put(callback, executor);
1140         return true;
1141     }
1142 
1143     /**
1144      * Unregisters a previously registered onDestroy callback
1145      *
1146      * @param callback The callback to remove.
1147      * @return whether the callback was unregistered.
1148      */
unregisterOnDestroyedCallback(@onNull Runnable callback)1149     public boolean unregisterOnDestroyedCallback(@NonNull Runnable callback) {
1150         Objects.requireNonNull(callback);
1151         if (isDestroyed()) {
1152             Log.w(TAG, "Cannot interact with a destroyed voice interactor");
1153             return false;
1154         }
1155         return mOnDestroyCallbacks.remove(callback) != null;
1156     }
1157 
1158     /**
1159      * Notifies the assist framework that the direct actions supported by the app changed.
1160      */
notifyDirectActionsChanged()1161     public void notifyDirectActionsChanged() {
1162         if (isDestroyed()) {
1163             Log.w(TAG, "Cannot interact with a destroyed voice interactor");
1164             return;
1165         }
1166         try {
1167             mInteractor.notifyDirectActionsChanged(mActivity.getTaskId(),
1168                     mActivity.getAssistToken());
1169         } catch (RemoteException e) {
1170             Log.w(TAG, "Voice interactor has died", e);
1171         }
1172     }
1173 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)1174     void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
1175         String innerPrefix = prefix + "    ";
1176         if (mActiveRequests.size() > 0) {
1177             writer.print(prefix); writer.println("Active voice requests:");
1178             for (int i=0; i<mActiveRequests.size(); i++) {
1179                 Request req = mActiveRequests.valueAt(i);
1180                 writer.print(prefix); writer.print("  #"); writer.print(i);
1181                 writer.print(": ");
1182                 writer.println(req);
1183                 req.dump(innerPrefix, fd, writer, args);
1184             }
1185         }
1186         writer.print(prefix); writer.println("VoiceInteractor misc state:");
1187         writer.print(prefix); writer.print("  mInteractor=");
1188         writer.println(mInteractor.asBinder());
1189         writer.print(prefix); writer.print("  mActivity="); writer.println(mActivity);
1190     }
1191 
1192     private static final class KillCallback extends ICancellationSignal.Stub {
1193         private final WeakReference<VoiceInteractor> mInteractor;
1194 
KillCallback(VoiceInteractor interactor)1195         KillCallback(VoiceInteractor interactor) {
1196             mInteractor= new WeakReference<>(interactor);
1197         }
1198 
1199         @Override
cancel()1200         public void cancel() {
1201             final VoiceInteractor voiceInteractor = mInteractor.get();
1202             if (voiceInteractor != null) {
1203                 voiceInteractor.mHandlerCaller.getHandler().sendMessage(PooledLambda
1204                         .obtainMessage(VoiceInteractor::destroy, voiceInteractor));
1205             }
1206         }
1207     }
1208 }
1209