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
17package android.app;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.content.Context;
22import android.os.Bundle;
23import android.os.IBinder;
24import android.os.Looper;
25import android.os.Message;
26import android.os.Parcel;
27import android.os.Parcelable;
28import android.os.RemoteException;
29import android.util.ArrayMap;
30import android.util.DebugUtils;
31import android.util.Log;
32import com.android.internal.app.IVoiceInteractor;
33import com.android.internal.app.IVoiceInteractorCallback;
34import com.android.internal.app.IVoiceInteractorRequest;
35import com.android.internal.os.HandlerCaller;
36import com.android.internal.os.SomeArgs;
37
38import java.io.FileDescriptor;
39import java.io.PrintWriter;
40import 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 */
64public 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
217        Request() {
218        }
219
220        /**
221         * Return the name this request was submitted through
222         * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}.
223         */
224        public String getName() {
225            return mName;
226        }
227
228        /**
229         * Cancel this active request.
230         */
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         */
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         */
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         */
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         */
270        public void onAttached(Activity activity) {
271        }
272
273        /**
274         * The request is being detached from an activity.
275         */
276        public void onDetached() {
277        }
278
279        @Override
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
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
298        String getRequestTypeName() {
299            return "Request";
300        }
301
302        void clear() {
303            mRequestInterface = null;
304            mContext = null;
305            mActivity = null;
306            mName = null;
307        }
308
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         */
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         */
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         */
360        public void onConfirmationResult(boolean confirmed, Bundle result) {
361        }
362
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
371        String getRequestTypeName() {
372            return "Confirmation";
373        }
374
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             */
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             */
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             */
437            public Option addSynonym(CharSequence synonym) {
438                if (mSynonyms == null) {
439                    mSynonyms = new ArrayList<>();
440                }
441                mSynonyms.add(synonym);
442                return this;
443            }
444
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             */
453            public int getIndex() {
454                return mIndex;
455            }
456
457            public int countSynonyms() {
458                return mSynonyms != null ? mSynonyms.size() : 0;
459            }
460
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             */
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             */
479            public Bundle getExtras() {
480                return mExtras;
481            }
482
483            Option(Parcel in) {
484                mLabel = in.readCharSequence();
485                mIndex = in.readInt();
486                mSynonyms = in.readCharSequenceList();
487                mExtras = in.readBundle();
488            }
489
490            @Override
491            public int describeContents() {
492                return 0;
493            }
494
495            @Override
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         */
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         */
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         */
554        public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
555        }
556
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
585        String getRequestTypeName() {
586            return "PickOption";
587        }
588
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         */
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         */
626        public CompleteVoiceRequest(CharSequence message, Bundle extras) {
627            mPrompt = (message != null ? new Prompt(message) : null);
628            mExtras = extras;
629        }
630
631        public void onCompleteResult(Bundle result) {
632        }
633
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
642        String getRequestTypeName() {
643            return "CompleteVoice";
644        }
645
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         */
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         */
685        public AbortVoiceRequest(CharSequence message, Bundle extras) {
686            mPrompt = (message != null ? new Prompt(message) : null);
687            mExtras = extras;
688        }
689
690        public void onAbortResult(Bundle result) {
691        }
692
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
701        String getRequestTypeName() {
702            return "AbortVoice";
703        }
704
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         */
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         */
745        public void onCommandResult(boolean isCompleted, Bundle result) {
746        }
747
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
756        String getRequestTypeName() {
757            return "Command";
758        }
759
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         */
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         */
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
812        public CharSequence getVoicePromptAt(int index) {
813            return mVoicePrompts[index];
814        }
815
816        /**
817         * Returns the number of different voice prompts.
818         */
819        public int countVoicePrompts() {
820            return mVoicePrompts.length;
821        }
822
823        /**
824         * Returns the prompt to use for visual display.
825         */
826        @NonNull
827        public CharSequence getVisualPrompt() {
828            return mVisualPrompt;
829        }
830
831        @Override
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. */
856        Prompt(Parcel in) {
857            mVoicePrompts = in.readCharSequenceArray();
858            mVisualPrompt = in.readCharSequence();
859        }
860
861        @Override
862        public int describeContents() {
863            return 0;
864        }
865
866        @Override
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
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
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
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
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
932    void retainInstance() {
933        mRetaining = true;
934    }
935
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
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     */
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     */
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     */
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     */
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
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