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