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