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