VoiceInteractor.java revision d3fdb8bed8e836786253f9cd5ab640c7c5ed8501
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.content.Context;
20import android.os.Bundle;
21import android.os.IBinder;
22import android.os.Looper;
23import android.os.Message;
24import android.os.RemoteException;
25import android.util.ArrayMap;
26import android.util.Log;
27import com.android.internal.app.IVoiceInteractor;
28import com.android.internal.app.IVoiceInteractorCallback;
29import com.android.internal.app.IVoiceInteractorRequest;
30import com.android.internal.os.HandlerCaller;
31import com.android.internal.os.SomeArgs;
32
33import java.util.ArrayList;
34
35/**
36 * Interface for an {@link Activity} to interact with the user through voice.  Use
37 * {@link android.app.Activity#getVoiceInteractor() Activity.getVoiceInteractor}
38 * to retrieve the interface, if the activity is currently involved in a voice interaction.
39 *
40 * <p>The voice interactor revolves around submitting voice interaction requests to the
41 * back-end voice interaction service that is working with the user.  These requests are
42 * submitted with {@link #submitRequest}, providing a new instance of a
43 * {@link Request} subclass describing the type of operation to perform -- currently the
44 * possible requests are {@link ConfirmationRequest} and {@link CommandRequest}.
45 *
46 * <p>Once a request is submitted, the voice system will process it and eventually deliver
47 * the result to the request object.  The application can cancel a pending request at any
48 * time.
49 *
50 * <p>The VoiceInteractor is integrated with Activity's state saving mechanism, so that
51 * if an activity is being restarted with retained state, it will retain the current
52 * VoiceInteractor and any outstanding requests.  Because of this, you should always use
53 * {@link Request#getActivity() Request.getActivity} to get back to the activity of a
54 * request, rather than holding on to the activity instance yourself, either explicitly
55 * or implicitly through a non-static inner class.
56 */
57public class VoiceInteractor {
58    static final String TAG = "VoiceInteractor";
59    static final boolean DEBUG = true;
60
61    final IVoiceInteractor mInteractor;
62
63    Context mContext;
64    Activity mActivity;
65
66    final HandlerCaller mHandlerCaller;
67    final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() {
68        @Override
69        public void executeMessage(Message msg) {
70            SomeArgs args = (SomeArgs)msg.obj;
71            Request request;
72            switch (msg.what) {
73                case MSG_CONFIRMATION_RESULT:
74                    request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
75                    if (DEBUG) Log.d(TAG, "onConfirmResult: req="
76                            + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
77                            + " confirmed=" + msg.arg1 + " result=" + args.arg2);
78                    if (request != null) {
79                        ((ConfirmationRequest)request).onConfirmationResult(msg.arg1 != 0,
80                                (Bundle) args.arg2);
81                        request.clear();
82                    }
83                    break;
84                case MSG_COMPLETE_VOICE_RESULT:
85                    request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
86                    if (DEBUG) Log.d(TAG, "onCompleteVoice: req="
87                            + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
88                            + " result=" + args.arg1);
89                    if (request != null) {
90                        ((CompleteVoiceRequest)request).onCompleteResult((Bundle) args.arg2);
91                        request.clear();
92                    }
93                    break;
94                case MSG_ABORT_VOICE_RESULT:
95                    request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
96                    if (DEBUG) Log.d(TAG, "onAbortVoice: req="
97                            + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
98                            + " result=" + args.arg1);
99                    if (request != null) {
100                        ((AbortVoiceRequest)request).onAbortResult((Bundle) args.arg2);
101                        request.clear();
102                    }
103                    break;
104                case MSG_COMMAND_RESULT:
105                    request = pullRequest((IVoiceInteractorRequest)args.arg1, msg.arg1 != 0);
106                    if (DEBUG) Log.d(TAG, "onCommandResult: req="
107                            + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request
108                            + " result=" + args.arg2);
109                    if (request != null) {
110                        ((CommandRequest)request).onCommandResult((Bundle) args.arg2);
111                        if (msg.arg1 != 0) {
112                            request.clear();
113                        }
114                    }
115                    break;
116                case MSG_CANCEL_RESULT:
117                    request = pullRequest((IVoiceInteractorRequest)args.arg1, true);
118                    if (DEBUG) Log.d(TAG, "onCancelResult: req="
119                            + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request);
120                    if (request != null) {
121                        request.onCancel();
122                        request.clear();
123                    }
124                    break;
125            }
126        }
127    };
128
129    final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() {
130        @Override
131        public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean confirmed,
132                Bundle result) {
133            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
134                    MSG_CONFIRMATION_RESULT, confirmed ? 1 : 0, request, result));
135        }
136
137        @Override
138        public void deliverCompleteVoiceResult(IVoiceInteractorRequest request, Bundle result) {
139            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
140                    MSG_COMPLETE_VOICE_RESULT, request, result));
141        }
142
143        @Override
144        public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result) {
145            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO(
146                    MSG_ABORT_VOICE_RESULT, request, result));
147        }
148
149        @Override
150        public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete,
151                Bundle result) {
152            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO(
153                    MSG_COMMAND_RESULT, complete ? 1 : 0, request, result));
154        }
155
156        @Override
157        public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException {
158            mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(
159                    MSG_CANCEL_RESULT, request));
160        }
161    };
162
163    final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<IBinder, Request>();
164
165    static final int MSG_CONFIRMATION_RESULT = 1;
166    static final int MSG_COMPLETE_VOICE_RESULT = 2;
167    static final int MSG_ABORT_VOICE_RESULT = 3;
168    static final int MSG_COMMAND_RESULT = 4;
169    static final int MSG_CANCEL_RESULT = 5;
170
171    public static abstract class Request {
172        IVoiceInteractorRequest mRequestInterface;
173        Context mContext;
174        Activity mActivity;
175
176        public Request() {
177        }
178
179        public void cancel() {
180            try {
181                mRequestInterface.cancel();
182            } catch (RemoteException e) {
183                Log.w(TAG, "Voice interactor has died", e);
184            }
185        }
186
187        public Context getContext() {
188            return mContext;
189        }
190
191        public Activity getActivity() {
192            return mActivity;
193        }
194
195        public void onCancel() {
196        }
197
198        public void onAttached(Activity activity) {
199        }
200
201        public void onDetached() {
202        }
203
204        void clear() {
205            mRequestInterface = null;
206            mContext = null;
207            mActivity = null;
208        }
209
210        abstract IVoiceInteractorRequest submit(IVoiceInteractor interactor,
211                String packageName, IVoiceInteractorCallback callback) throws RemoteException;
212    }
213
214    public static class ConfirmationRequest extends Request {
215        final CharSequence mPrompt;
216        final Bundle mExtras;
217
218        /**
219         * Confirms an operation with the user via the trusted system
220         * VoiceInteractionService.  This allows an Activity to complete an unsafe operation that
221         * would require the user to touch the screen when voice interaction mode is not enabled.
222         * The result of the confirmation will be returned through an asynchronous call to
223         * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or
224         * {@link #onCancel()}.
225         *
226         * <p>In some cases this may be a simple yes / no confirmation or the confirmation could
227         * include context information about how the action will be completed
228         * (e.g. booking a cab might include details about how long until the cab arrives)
229         * so the user can give a confirmation.
230         * @param prompt Optional confirmation text to read to the user as the action being
231         * confirmed.
232         * @param extras Additional optional information.
233         */
234        public ConfirmationRequest(CharSequence prompt, Bundle extras) {
235            mPrompt = prompt;
236            mExtras = extras;
237        }
238
239        public void onConfirmationResult(boolean confirmed, Bundle result) {
240        }
241
242        IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
243                IVoiceInteractorCallback callback) throws RemoteException {
244            return interactor.startConfirmation(packageName, callback, mPrompt, mExtras);
245        }
246    }
247
248    public static class CompleteVoiceRequest extends Request {
249        final CharSequence mMessage;
250        final Bundle mExtras;
251
252        /**
253         * Reports that the current interaction was successfully completed with voice, so the
254         * application can report the final status to the user. When the response comes back, the
255         * voice system has handled the request and is ready to switch; at that point the
256         * application can start a new non-voice activity or finish.  Be sure when starting the new
257         * activity to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
258         * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
259         * interaction task.
260         *
261         * @param message Optional message to tell user about the completion status of the task.
262         * @param extras Additional optional information.
263         */
264        public CompleteVoiceRequest(CharSequence message, Bundle extras) {
265            mMessage = message;
266            mExtras = extras;
267        }
268
269        public void onCompleteResult(Bundle result) {
270        }
271
272        IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
273                IVoiceInteractorCallback callback) throws RemoteException {
274            return interactor.startCompleteVoice(packageName, callback, mMessage, mExtras);
275        }
276    }
277
278    public static class AbortVoiceRequest extends Request {
279        final CharSequence mMessage;
280        final Bundle mExtras;
281
282        /**
283         * Reports that the current interaction can not be complete with voice, so the
284         * application will need to switch to a traditional input UI.  Applications should
285         * only use this when they need to completely bail out of the voice interaction
286         * and switch to a traditional UI.  When the response comes back, the voice
287         * system has handled the request and is ready to switch; at that point the application
288         * can start a new non-voice activity.  Be sure when starting the new activity
289         * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK
290         * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice
291         * interaction task.
292         *
293         * @param message Optional message to tell user about not being able to complete
294         * the interaction with voice.
295         * @param extras Additional optional information.
296         */
297        public AbortVoiceRequest(CharSequence message, Bundle extras) {
298            mMessage = message;
299            mExtras = extras;
300        }
301
302        public void onAbortResult(Bundle result) {
303        }
304
305        IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
306                IVoiceInteractorCallback callback) throws RemoteException {
307            return interactor.startAbortVoice(packageName, callback, mMessage, mExtras);
308        }
309    }
310
311    public static class CommandRequest extends Request {
312        final String mCommand;
313        final Bundle mArgs;
314
315        /**
316         * Execute a command using the trusted system VoiceInteractionService.
317         * This allows an Activity to request additional information from the user needed to
318         * complete an action (e.g. booking a table might have several possible times that the
319         * user could select from or an app might need the user to agree to a terms of service).
320         * The result of the confirmation will be returned through an asynchronous call to
321         * either {@link #onCommandResult(android.os.Bundle)} or
322         * {@link #onCancel()}.
323         *
324         * <p>The command is a string that describes the generic operation to be performed.
325         * The command will determine how the properties in extras are interpreted and the set of
326         * available commands is expected to grow over time.  An example might be
327         * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of
328         * airline check-in.  (This is not an actual working example.)
329         *
330         * @param command The desired command to perform.
331         * @param args Additional arguments to control execution of the command.
332         */
333        public CommandRequest(String command, Bundle args) {
334            mCommand = command;
335            mArgs = args;
336        }
337
338        public void onCommandResult(Bundle result) {
339        }
340
341        IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
342                IVoiceInteractorCallback callback) throws RemoteException {
343            return interactor.startCommand(packageName, callback, mCommand, mArgs);
344        }
345   }
346
347    VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity,
348            Looper looper) {
349        mInteractor = interactor;
350        mContext = context;
351        mActivity = activity;
352        mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true);
353    }
354
355    Request pullRequest(IVoiceInteractorRequest request, boolean complete) {
356        synchronized (mActiveRequests) {
357            Request req = mActiveRequests.get(request.asBinder());
358            if (req != null && complete) {
359                mActiveRequests.remove(request.asBinder());
360            }
361            return req;
362        }
363    }
364
365    private ArrayList<Request> makeRequestList() {
366        final int N = mActiveRequests.size();
367        if (N < 1) {
368            return null;
369        }
370        ArrayList<Request> list = new ArrayList<Request>(N);
371        for (int i=0; i<N; i++) {
372            list.add(mActiveRequests.valueAt(i));
373        }
374        return list;
375    }
376
377    void attachActivity(Activity activity) {
378        if (mActivity == activity) {
379            return;
380        }
381        mContext = activity;
382        mActivity = activity;
383        ArrayList<Request> reqs = makeRequestList();
384        if (reqs != null) {
385            for (int i=0; i<reqs.size(); i++) {
386                Request req = reqs.get(i);
387                req.mContext = activity;
388                req.mActivity = activity;
389                req.onAttached(activity);
390            }
391        }
392    }
393
394    void detachActivity() {
395        ArrayList<Request> reqs = makeRequestList();
396        if (reqs != null) {
397            for (int i=0; i<reqs.size(); i++) {
398                Request req = reqs.get(i);
399                req.onDetached();
400                req.mActivity = null;
401                req.mContext = null;
402            }
403        }
404        mContext = null;
405        mActivity = null;
406    }
407
408    public boolean submitRequest(Request request) {
409        try {
410            IVoiceInteractorRequest ireq = request.submit(mInteractor,
411                    mContext.getOpPackageName(), mCallback);
412            request.mRequestInterface = ireq;
413            request.mContext = mContext;
414            request.mActivity = mActivity;
415            synchronized (mActiveRequests) {
416                mActiveRequests.put(ireq.asBinder(), request);
417            }
418            return true;
419        } catch (RemoteException e) {
420            Log.w(TAG, "Remove voice interactor service died", e);
421            return false;
422        }
423    }
424
425    /**
426     * Queries the supported commands available from the VoiceinteractionService.
427     * The command is a string that describes the generic operation to be performed.
428     * An example might be "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number
429     * of bags as part of airline check-in.  (This is not an actual working example.)
430     *
431     * @param commands
432     */
433    public boolean[] supportsCommands(String[] commands) {
434        try {
435            boolean[] res = mInteractor.supportsCommands(mContext.getOpPackageName(), commands);
436            if (DEBUG) Log.d(TAG, "supportsCommands: cmds=" + commands + " res=" + res);
437            return res;
438        } catch (RemoteException e) {
439            throw new RuntimeException("Voice interactor has died", e);
440        }
441    }
442}
443