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