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