1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app;
18 
19 import android.annotation.SystemApi;
20 import android.content.Context;
21 import android.os.Bundle;
22 import android.os.IBinder;
23 import android.os.Looper;
24 import android.os.Message;
25 import android.os.RemoteException;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 import com.android.internal.app.IVoiceInteractor;
29 import com.android.internal.app.IVoiceInteractorCallback;
30 import com.android.internal.app.IVoiceInteractorRequest;
31 import com.android.internal.os.HandlerCaller;
32 import com.android.internal.os.SomeArgs;
33 
34 import 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
60 public 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 
Request()179         public Request() {
180         }
181 
cancel()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 
getContext()190         public Context getContext() {
191             return mContext;
192         }
193 
getActivity()194         public Activity getActivity() {
195             return mActivity;
196         }
197 
onCancel()198         public void onCancel() {
199         }
200 
onAttached(Activity activity)201         public void onAttached(Activity activity) {
202         }
203 
onDetached()204         public void onDetached() {
205         }
206 
clear()207         void clear() {
208             mRequestInterface = null;
209             mContext = null;
210             mActivity = null;
211         }
212 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)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          */
ConfirmationRequest(CharSequence prompt, Bundle extras)237         public ConfirmationRequest(CharSequence prompt, Bundle extras) {
238             mPrompt = prompt;
239             mExtras = extras;
240         }
241 
onConfirmationResult(boolean confirmed, Bundle result)242         public void onConfirmationResult(boolean confirmed, Bundle result) {
243         }
244 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)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          */
CompleteVoiceRequest(CharSequence message, Bundle extras)267         public CompleteVoiceRequest(CharSequence message, Bundle extras) {
268             mMessage = message;
269             mExtras = extras;
270         }
271 
onCompleteResult(Bundle result)272         public void onCompleteResult(Bundle result) {
273         }
274 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)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          */
AbortVoiceRequest(CharSequence message, Bundle extras)300         public AbortVoiceRequest(CharSequence message, Bundle extras) {
301             mMessage = message;
302             mExtras = extras;
303         }
304 
onAbortResult(Bundle result)305         public void onAbortResult(Bundle result) {
306         }
307 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)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          */
CommandRequest(String command, Bundle args)336         public CommandRequest(String command, Bundle args) {
337             mCommand = command;
338             mArgs = args;
339         }
340 
onCommandResult(Bundle result)341         public void onCommandResult(Bundle result) {
342         }
343 
submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)344         IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName,
345                 IVoiceInteractorCallback callback) throws RemoteException {
346             return interactor.startCommand(packageName, callback, mCommand, mArgs);
347         }
348    }
349 
VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity, Looper looper)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 
pullRequest(IVoiceInteractorRequest request, boolean complete)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 
makeRequestList()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 
attachActivity(Activity activity)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 
detachActivity()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 
submitRequest(Request request)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      */
supportsCommands(String[] commands)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