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