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