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.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.os.Bundle; 23 import android.os.IBinder; 24 import android.os.Looper; 25 import android.os.Message; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.os.RemoteException; 29 import android.util.ArrayMap; 30 import android.util.DebugUtils; 31 import android.util.Log; 32 import com.android.internal.app.IVoiceInteractor; 33 import com.android.internal.app.IVoiceInteractorCallback; 34 import com.android.internal.app.IVoiceInteractorRequest; 35 import com.android.internal.os.HandlerCaller; 36 import com.android.internal.os.SomeArgs; 37 38 import java.io.FileDescriptor; 39 import java.io.PrintWriter; 40 import java.util.ArrayList; 41 42 /** 43 * Interface for an {@link Activity} to interact with the user through voice. Use 44 * {@link android.app.Activity#getVoiceInteractor() Activity.getVoiceInteractor} 45 * to retrieve the interface, if the activity is currently involved in a voice interaction. 46 * 47 * <p>The voice interactor revolves around submitting voice interaction requests to the 48 * back-end voice interaction service that is working with the user. These requests are 49 * submitted with {@link #submitRequest}, providing a new instance of a 50 * {@link Request} subclass describing the type of operation to perform -- currently the 51 * possible requests are {@link ConfirmationRequest} and {@link CommandRequest}. 52 * 53 * <p>Once a request is submitted, the voice system will process it and eventually deliver 54 * the result to the request object. The application can cancel a pending request at any 55 * time. 56 * 57 * <p>The VoiceInteractor is integrated with Activity's state saving mechanism, so that 58 * if an activity is being restarted with retained state, it will retain the current 59 * VoiceInteractor and any outstanding requests. Because of this, you should always use 60 * {@link Request#getActivity() Request.getActivity} to get back to the activity of a 61 * request, rather than holding on to the activity instance yourself, either explicitly 62 * or implicitly through a non-static inner class. 63 */ 64 public final class VoiceInteractor { 65 static final String TAG = "VoiceInteractor"; 66 static final boolean DEBUG = false; 67 68 static final Request[] NO_REQUESTS = new Request[0]; 69 70 final IVoiceInteractor mInteractor; 71 72 Context mContext; 73 Activity mActivity; 74 boolean mRetaining; 75 76 final HandlerCaller mHandlerCaller; 77 final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() { 78 @Override 79 public void executeMessage(Message msg) { 80 SomeArgs args = (SomeArgs)msg.obj; 81 Request request; 82 boolean complete; 83 switch (msg.what) { 84 case MSG_CONFIRMATION_RESULT: 85 request = pullRequest((IVoiceInteractorRequest)args.arg1, true); 86 if (DEBUG) Log.d(TAG, "onConfirmResult: req=" 87 + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request 88 + " confirmed=" + msg.arg1 + " result=" + args.arg2); 89 if (request != null) { 90 ((ConfirmationRequest)request).onConfirmationResult(msg.arg1 != 0, 91 (Bundle) args.arg2); 92 request.clear(); 93 } 94 break; 95 case MSG_PICK_OPTION_RESULT: 96 complete = msg.arg1 != 0; 97 request = pullRequest((IVoiceInteractorRequest)args.arg1, complete); 98 if (DEBUG) Log.d(TAG, "onPickOptionResult: req=" 99 + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request 100 + " finished=" + complete + " selection=" + args.arg2 101 + " result=" + args.arg3); 102 if (request != null) { 103 ((PickOptionRequest)request).onPickOptionResult(complete, 104 (PickOptionRequest.Option[]) args.arg2, (Bundle) args.arg3); 105 if (complete) { 106 request.clear(); 107 } 108 } 109 break; 110 case MSG_COMPLETE_VOICE_RESULT: 111 request = pullRequest((IVoiceInteractorRequest)args.arg1, true); 112 if (DEBUG) Log.d(TAG, "onCompleteVoice: req=" 113 + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request 114 + " result=" + args.arg2); 115 if (request != null) { 116 ((CompleteVoiceRequest)request).onCompleteResult((Bundle) args.arg2); 117 request.clear(); 118 } 119 break; 120 case MSG_ABORT_VOICE_RESULT: 121 request = pullRequest((IVoiceInteractorRequest)args.arg1, true); 122 if (DEBUG) Log.d(TAG, "onAbortVoice: req=" 123 + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request 124 + " result=" + args.arg2); 125 if (request != null) { 126 ((AbortVoiceRequest)request).onAbortResult((Bundle) args.arg2); 127 request.clear(); 128 } 129 break; 130 case MSG_COMMAND_RESULT: 131 complete = msg.arg1 != 0; 132 request = pullRequest((IVoiceInteractorRequest)args.arg1, complete); 133 if (DEBUG) Log.d(TAG, "onCommandResult: req=" 134 + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request 135 + " completed=" + msg.arg1 + " result=" + args.arg2); 136 if (request != null) { 137 ((CommandRequest)request).onCommandResult(msg.arg1 != 0, 138 (Bundle) args.arg2); 139 if (complete) { 140 request.clear(); 141 } 142 } 143 break; 144 case MSG_CANCEL_RESULT: 145 request = pullRequest((IVoiceInteractorRequest)args.arg1, true); 146 if (DEBUG) Log.d(TAG, "onCancelResult: req=" 147 + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request); 148 if (request != null) { 149 request.onCancel(); 150 request.clear(); 151 } 152 break; 153 } 154 } 155 }; 156 157 final IVoiceInteractorCallback.Stub mCallback = new IVoiceInteractorCallback.Stub() { 158 @Override 159 public void deliverConfirmationResult(IVoiceInteractorRequest request, boolean finished, 160 Bundle result) { 161 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO( 162 MSG_CONFIRMATION_RESULT, finished ? 1 : 0, request, result)); 163 } 164 165 @Override 166 public void deliverPickOptionResult(IVoiceInteractorRequest request, 167 boolean finished, PickOptionRequest.Option[] options, Bundle result) { 168 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOOO( 169 MSG_PICK_OPTION_RESULT, finished ? 1 : 0, request, options, result)); 170 } 171 172 @Override 173 public void deliverCompleteVoiceResult(IVoiceInteractorRequest request, Bundle result) { 174 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO( 175 MSG_COMPLETE_VOICE_RESULT, request, result)); 176 } 177 178 @Override 179 public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result) { 180 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO( 181 MSG_ABORT_VOICE_RESULT, request, result)); 182 } 183 184 @Override 185 public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete, 186 Bundle result) { 187 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO( 188 MSG_COMMAND_RESULT, complete ? 1 : 0, request, result)); 189 } 190 191 @Override 192 public void deliverCancel(IVoiceInteractorRequest request) throws RemoteException { 193 mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO( 194 MSG_CANCEL_RESULT, request, null)); 195 } 196 }; 197 198 final ArrayMap<IBinder, Request> mActiveRequests = new ArrayMap<>(); 199 200 static final int MSG_CONFIRMATION_RESULT = 1; 201 static final int MSG_PICK_OPTION_RESULT = 2; 202 static final int MSG_COMPLETE_VOICE_RESULT = 3; 203 static final int MSG_ABORT_VOICE_RESULT = 4; 204 static final int MSG_COMMAND_RESULT = 5; 205 static final int MSG_CANCEL_RESULT = 6; 206 207 /** 208 * Base class for voice interaction requests that can be submitted to the interactor. 209 * Do not instantiate this directly -- instead, use the appropriate subclass. 210 */ 211 public static abstract class Request { 212 IVoiceInteractorRequest mRequestInterface; 213 Context mContext; 214 Activity mActivity; 215 String mName; 216 Request()217 Request() { 218 } 219 220 /** 221 * Return the name this request was submitted through 222 * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}. 223 */ getName()224 public String getName() { 225 return mName; 226 } 227 228 /** 229 * Cancel this active request. 230 */ cancel()231 public void cancel() { 232 if (mRequestInterface == null) { 233 throw new IllegalStateException("Request " + this + " is no longer active"); 234 } 235 try { 236 mRequestInterface.cancel(); 237 } catch (RemoteException e) { 238 Log.w(TAG, "Voice interactor has died", e); 239 } 240 } 241 242 /** 243 * Return the current {@link Context} this request is associated with. May change 244 * if the activity hosting it goes through a configuration change. 245 */ getContext()246 public Context getContext() { 247 return mContext; 248 } 249 250 /** 251 * Return the current {@link Activity} this request is associated with. Will change 252 * if the activity is restarted such as through a configuration change. 253 */ getActivity()254 public Activity getActivity() { 255 return mActivity; 256 } 257 258 /** 259 * Report from voice interaction service: this operation has been canceled, typically 260 * as a completion of a previous call to {@link #cancel} or when the user explicitly 261 * cancelled. 262 */ onCancel()263 public void onCancel() { 264 } 265 266 /** 267 * The request is now attached to an activity, or being re-attached to a new activity 268 * after a configuration change. 269 */ onAttached(Activity activity)270 public void onAttached(Activity activity) { 271 } 272 273 /** 274 * The request is being detached from an activity. 275 */ onDetached()276 public void onDetached() { 277 } 278 279 @Override toString()280 public String toString() { 281 StringBuilder sb = new StringBuilder(128); 282 DebugUtils.buildShortClassTag(this, sb); 283 sb.append(" "); 284 sb.append(getRequestTypeName()); 285 sb.append(" name="); 286 sb.append(mName); 287 sb.append('}'); 288 return sb.toString(); 289 } 290 dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)291 void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 292 writer.print(prefix); writer.print("mRequestInterface="); 293 writer.println(mRequestInterface.asBinder()); 294 writer.print(prefix); writer.print("mActivity="); writer.println(mActivity); 295 writer.print(prefix); writer.print("mName="); writer.println(mName); 296 } 297 getRequestTypeName()298 String getRequestTypeName() { 299 return "Request"; 300 } 301 clear()302 void clear() { 303 mRequestInterface = null; 304 mContext = null; 305 mActivity = null; 306 mName = null; 307 } 308 submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)309 abstract IVoiceInteractorRequest submit(IVoiceInteractor interactor, 310 String packageName, IVoiceInteractorCallback callback) throws RemoteException; 311 } 312 313 /** 314 * Confirms an operation with the user via the trusted system 315 * VoiceInteractionService. This allows an Activity to complete an unsafe operation that 316 * would require the user to touch the screen when voice interaction mode is not enabled. 317 * The result of the confirmation will be returned through an asynchronous call to 318 * either {@link #onConfirmationResult(boolean, android.os.Bundle)} or 319 * {@link #onCancel()} - these methods should be overridden to define the application specific 320 * behavior. 321 * 322 * <p>In some cases this may be a simple yes / no confirmation or the confirmation could 323 * include context information about how the action will be completed 324 * (e.g. booking a cab might include details about how long until the cab arrives) 325 * so the user can give a confirmation. 326 */ 327 public static class ConfirmationRequest extends Request { 328 final Prompt mPrompt; 329 final Bundle mExtras; 330 331 /** 332 * Create a new confirmation request. 333 * @param prompt Optional confirmation to speak to the user or null if nothing 334 * should be spoken. 335 * @param extras Additional optional information or null. 336 */ ConfirmationRequest(@ullable Prompt prompt, @Nullable Bundle extras)337 public ConfirmationRequest(@Nullable Prompt prompt, @Nullable Bundle extras) { 338 mPrompt = prompt; 339 mExtras = extras; 340 } 341 342 /** 343 * Create a new confirmation request. 344 * @param prompt Optional confirmation to speak to the user or null if nothing 345 * should be spoken. 346 * @param extras Additional optional information or null. 347 * @hide 348 */ ConfirmationRequest(CharSequence prompt, Bundle extras)349 public ConfirmationRequest(CharSequence prompt, Bundle extras) { 350 mPrompt = (prompt != null ? new Prompt(prompt) : null); 351 mExtras = extras; 352 } 353 354 /** 355 * Handle the confirmation result. Override this method to define 356 * the behavior when the user confirms or rejects the operation. 357 * @param confirmed Whether the user confirmed or rejected the operation. 358 * @param result Additional result information or null. 359 */ onConfirmationResult(boolean confirmed, Bundle result)360 public void onConfirmationResult(boolean confirmed, Bundle result) { 361 } 362 dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)363 void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 364 super.dump(prefix, fd, writer, args); 365 writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt); 366 if (mExtras != null) { 367 writer.print(prefix); writer.print("mExtras="); writer.println(mExtras); 368 } 369 } 370 getRequestTypeName()371 String getRequestTypeName() { 372 return "Confirmation"; 373 } 374 submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)375 IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, 376 IVoiceInteractorCallback callback) throws RemoteException { 377 return interactor.startConfirmation(packageName, callback, mPrompt, mExtras); 378 } 379 } 380 381 /** 382 * Select a single option from multiple potential options with the user via the trusted system 383 * VoiceInteractionService. Typically, the application would present this visually as 384 * a list view to allow selecting the option by touch. 385 * The result of the confirmation will be returned through an asynchronous call to 386 * either {@link #onPickOptionResult} or {@link #onCancel()} - these methods should 387 * be overridden to define the application specific behavior. 388 */ 389 public static class PickOptionRequest extends Request { 390 final Prompt mPrompt; 391 final Option[] mOptions; 392 final Bundle mExtras; 393 394 /** 395 * Represents a single option that the user may select using their voice. The 396 * {@link #getIndex()} method should be used as a unique ID to identify the option 397 * when it is returned from the voice interactor. 398 */ 399 public static final class Option implements Parcelable { 400 final CharSequence mLabel; 401 final int mIndex; 402 ArrayList<CharSequence> mSynonyms; 403 Bundle mExtras; 404 405 /** 406 * Creates an option that a user can select with their voice by matching the label 407 * or one of several synonyms. 408 * @param label The label that will both be matched against what the user speaks 409 * and displayed visually. 410 * @hide 411 */ Option(CharSequence label)412 public Option(CharSequence label) { 413 mLabel = label; 414 mIndex = -1; 415 } 416 417 /** 418 * Creates an option that a user can select with their voice by matching the label 419 * or one of several synonyms. 420 * @param label The label that will both be matched against what the user speaks 421 * and displayed visually. 422 * @param index The location of this option within the overall set of options. 423 * Can be used to help identify the option when it is returned from the 424 * voice interactor. 425 */ Option(CharSequence label, int index)426 public Option(CharSequence label, int index) { 427 mLabel = label; 428 mIndex = index; 429 } 430 431 /** 432 * Add a synonym term to the option to indicate an alternative way the content 433 * may be matched. 434 * @param synonym The synonym that will be matched against what the user speaks, 435 * but not displayed. 436 */ addSynonym(CharSequence synonym)437 public Option addSynonym(CharSequence synonym) { 438 if (mSynonyms == null) { 439 mSynonyms = new ArrayList<>(); 440 } 441 mSynonyms.add(synonym); 442 return this; 443 } 444 getLabel()445 public CharSequence getLabel() { 446 return mLabel; 447 } 448 449 /** 450 * Return the index that was supplied in the constructor. 451 * If the option was constructed without an index, -1 is returned. 452 */ getIndex()453 public int getIndex() { 454 return mIndex; 455 } 456 countSynonyms()457 public int countSynonyms() { 458 return mSynonyms != null ? mSynonyms.size() : 0; 459 } 460 getSynonymAt(int index)461 public CharSequence getSynonymAt(int index) { 462 return mSynonyms != null ? mSynonyms.get(index) : null; 463 } 464 465 /** 466 * Set optional extra information associated with this option. Note that this 467 * method takes ownership of the supplied extras Bundle. 468 */ setExtras(Bundle extras)469 public void setExtras(Bundle extras) { 470 mExtras = extras; 471 } 472 473 /** 474 * Return any optional extras information associated with this option, or null 475 * if there is none. Note that this method returns a reference to the actual 476 * extras Bundle in the option, so modifications to it will directly modify the 477 * extras in the option. 478 */ getExtras()479 public Bundle getExtras() { 480 return mExtras; 481 } 482 Option(Parcel in)483 Option(Parcel in) { 484 mLabel = in.readCharSequence(); 485 mIndex = in.readInt(); 486 mSynonyms = in.readCharSequenceList(); 487 mExtras = in.readBundle(); 488 } 489 490 @Override describeContents()491 public int describeContents() { 492 return 0; 493 } 494 495 @Override writeToParcel(Parcel dest, int flags)496 public void writeToParcel(Parcel dest, int flags) { 497 dest.writeCharSequence(mLabel); 498 dest.writeInt(mIndex); 499 dest.writeCharSequenceList(mSynonyms); 500 dest.writeBundle(mExtras); 501 } 502 503 public static final Parcelable.Creator<Option> CREATOR 504 = new Parcelable.Creator<Option>() { 505 public Option createFromParcel(Parcel in) { 506 return new Option(in); 507 } 508 509 public Option[] newArray(int size) { 510 return new Option[size]; 511 } 512 }; 513 }; 514 515 /** 516 * Create a new pick option request. 517 * @param prompt Optional question to be asked of the user when the options are 518 * presented or null if nothing should be asked. 519 * @param options The set of {@link Option}s the user is selecting from. 520 * @param extras Additional optional information or null. 521 */ PickOptionRequest(@ullable Prompt prompt, Option[] options, @Nullable Bundle extras)522 public PickOptionRequest(@Nullable Prompt prompt, Option[] options, 523 @Nullable Bundle extras) { 524 mPrompt = prompt; 525 mOptions = options; 526 mExtras = extras; 527 } 528 529 /** 530 * Create a new pick option request. 531 * @param prompt Optional question to be asked of the user when the options are 532 * presented or null if nothing should be asked. 533 * @param options The set of {@link Option}s the user is selecting from. 534 * @param extras Additional optional information or null. 535 * @hide 536 */ PickOptionRequest(CharSequence prompt, Option[] options, Bundle extras)537 public PickOptionRequest(CharSequence prompt, Option[] options, Bundle extras) { 538 mPrompt = (prompt != null ? new Prompt(prompt) : null); 539 mOptions = options; 540 mExtras = extras; 541 } 542 543 /** 544 * Called when a single option is confirmed or narrowed to one of several options. Override 545 * this method to define the behavior when the user selects an option or narrows down the 546 * set of options. 547 * @param finished True if the voice interaction has finished making a selection, in 548 * which case {@code selections} contains the final result. If false, this request is 549 * still active and you will continue to get calls on it. 550 * @param selections Either a single {@link Option} or one of several {@link Option}s the 551 * user has narrowed the choices down to. 552 * @param result Additional optional information. 553 */ onPickOptionResult(boolean finished, Option[] selections, Bundle result)554 public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) { 555 } 556 dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)557 void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 558 super.dump(prefix, fd, writer, args); 559 writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt); 560 if (mOptions != null) { 561 writer.print(prefix); writer.println("Options:"); 562 for (int i=0; i<mOptions.length; i++) { 563 Option op = mOptions[i]; 564 writer.print(prefix); writer.print(" #"); writer.print(i); writer.println(":"); 565 writer.print(prefix); writer.print(" mLabel="); writer.println(op.mLabel); 566 writer.print(prefix); writer.print(" mIndex="); writer.println(op.mIndex); 567 if (op.mSynonyms != null && op.mSynonyms.size() > 0) { 568 writer.print(prefix); writer.println(" Synonyms:"); 569 for (int j=0; j<op.mSynonyms.size(); j++) { 570 writer.print(prefix); writer.print(" #"); writer.print(j); 571 writer.print(": "); writer.println(op.mSynonyms.get(j)); 572 } 573 } 574 if (op.mExtras != null) { 575 writer.print(prefix); writer.print(" mExtras="); 576 writer.println(op.mExtras); 577 } 578 } 579 } 580 if (mExtras != null) { 581 writer.print(prefix); writer.print("mExtras="); writer.println(mExtras); 582 } 583 } 584 getRequestTypeName()585 String getRequestTypeName() { 586 return "PickOption"; 587 } 588 submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)589 IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, 590 IVoiceInteractorCallback callback) throws RemoteException { 591 return interactor.startPickOption(packageName, callback, mPrompt, mOptions, mExtras); 592 } 593 } 594 595 /** 596 * Reports that the current interaction was successfully completed with voice, so the 597 * application can report the final status to the user. When the response comes back, the 598 * voice system has handled the request and is ready to switch; at that point the 599 * application can start a new non-voice activity or finish. Be sure when starting the new 600 * activity to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK 601 * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice 602 * interaction task. 603 */ 604 public static class CompleteVoiceRequest extends Request { 605 final Prompt mPrompt; 606 final Bundle mExtras; 607 608 /** 609 * Create a new completed voice interaction request. 610 * @param prompt Optional message to speak to the user about the completion status of 611 * the task or null if nothing should be spoken. 612 * @param extras Additional optional information or null. 613 */ CompleteVoiceRequest(@ullable Prompt prompt, @Nullable Bundle extras)614 public CompleteVoiceRequest(@Nullable Prompt prompt, @Nullable Bundle extras) { 615 mPrompt = prompt; 616 mExtras = extras; 617 } 618 619 /** 620 * Create a new completed voice interaction request. 621 * @param message Optional message to speak to the user about the completion status of 622 * the task or null if nothing should be spoken. 623 * @param extras Additional optional information or null. 624 * @hide 625 */ CompleteVoiceRequest(CharSequence message, Bundle extras)626 public CompleteVoiceRequest(CharSequence message, Bundle extras) { 627 mPrompt = (message != null ? new Prompt(message) : null); 628 mExtras = extras; 629 } 630 onCompleteResult(Bundle result)631 public void onCompleteResult(Bundle result) { 632 } 633 dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)634 void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 635 super.dump(prefix, fd, writer, args); 636 writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt); 637 if (mExtras != null) { 638 writer.print(prefix); writer.print("mExtras="); writer.println(mExtras); 639 } 640 } 641 getRequestTypeName()642 String getRequestTypeName() { 643 return "CompleteVoice"; 644 } 645 submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)646 IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, 647 IVoiceInteractorCallback callback) throws RemoteException { 648 return interactor.startCompleteVoice(packageName, callback, mPrompt, mExtras); 649 } 650 } 651 652 /** 653 * Reports that the current interaction can not be complete with voice, so the 654 * application will need to switch to a traditional input UI. Applications should 655 * only use this when they need to completely bail out of the voice interaction 656 * and switch to a traditional UI. When the response comes back, the voice 657 * system has handled the request and is ready to switch; at that point the application 658 * can start a new non-voice activity. Be sure when starting the new activity 659 * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK 660 * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice 661 * interaction task. 662 */ 663 public static class AbortVoiceRequest extends Request { 664 final Prompt mPrompt; 665 final Bundle mExtras; 666 667 /** 668 * Create a new voice abort request. 669 * @param prompt Optional message to speak to the user indicating why the task could 670 * not be completed by voice or null if nothing should be spoken. 671 * @param extras Additional optional information or null. 672 */ AbortVoiceRequest(@ullable Prompt prompt, @Nullable Bundle extras)673 public AbortVoiceRequest(@Nullable Prompt prompt, @Nullable Bundle extras) { 674 mPrompt = prompt; 675 mExtras = extras; 676 } 677 678 /** 679 * Create a new voice abort request. 680 * @param message Optional message to speak to the user indicating why the task could 681 * not be completed by voice or null if nothing should be spoken. 682 * @param extras Additional optional information or null. 683 * @hide 684 */ AbortVoiceRequest(CharSequence message, Bundle extras)685 public AbortVoiceRequest(CharSequence message, Bundle extras) { 686 mPrompt = (message != null ? new Prompt(message) : null); 687 mExtras = extras; 688 } 689 onAbortResult(Bundle result)690 public void onAbortResult(Bundle result) { 691 } 692 dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)693 void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 694 super.dump(prefix, fd, writer, args); 695 writer.print(prefix); writer.print("mPrompt="); writer.println(mPrompt); 696 if (mExtras != null) { 697 writer.print(prefix); writer.print("mExtras="); writer.println(mExtras); 698 } 699 } 700 getRequestTypeName()701 String getRequestTypeName() { 702 return "AbortVoice"; 703 } 704 submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)705 IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, 706 IVoiceInteractorCallback callback) throws RemoteException { 707 return interactor.startAbortVoice(packageName, callback, mPrompt, mExtras); 708 } 709 } 710 711 /** 712 * Execute a vendor-specific command using the trusted system VoiceInteractionService. 713 * This allows an Activity to request additional information from the user needed to 714 * complete an action (e.g. booking a table might have several possible times that the 715 * user could select from or an app might need the user to agree to a terms of service). 716 * The result of the confirmation will be returned through an asynchronous call to 717 * either {@link #onCommandResult(boolean, android.os.Bundle)} or 718 * {@link #onCancel()}. 719 * 720 * <p>The command is a string that describes the generic operation to be performed. 721 * The command will determine how the properties in extras are interpreted and the set of 722 * available commands is expected to grow over time. An example might be 723 * "com.google.voice.commands.REQUEST_NUMBER_BAGS" to request the number of bags as part of 724 * airline check-in. (This is not an actual working example.) 725 */ 726 public static class CommandRequest extends Request { 727 final String mCommand; 728 final Bundle mArgs; 729 730 /** 731 * Create a new generic command request. 732 * @param command The desired command to perform. 733 * @param args Additional arguments to control execution of the command. 734 */ CommandRequest(String command, Bundle args)735 public CommandRequest(String command, Bundle args) { 736 mCommand = command; 737 mArgs = args; 738 } 739 740 /** 741 * Results for CommandRequest can be returned in partial chunks. 742 * The isCompleted is set to true iff all results have been returned, indicating the 743 * CommandRequest has completed. 744 */ onCommandResult(boolean isCompleted, Bundle result)745 public void onCommandResult(boolean isCompleted, Bundle result) { 746 } 747 dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)748 void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 749 super.dump(prefix, fd, writer, args); 750 writer.print(prefix); writer.print("mCommand="); writer.println(mCommand); 751 if (mArgs != null) { 752 writer.print(prefix); writer.print("mArgs="); writer.println(mArgs); 753 } 754 } 755 getRequestTypeName()756 String getRequestTypeName() { 757 return "Command"; 758 } 759 submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback)760 IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, 761 IVoiceInteractorCallback callback) throws RemoteException { 762 return interactor.startCommand(packageName, callback, mCommand, mArgs); 763 } 764 } 765 766 /** 767 * A set of voice prompts to use with the voice interaction system to confirm an action, select 768 * an option, or do similar operations. Multiple voice prompts may be provided for variety. A 769 * visual prompt must be provided, which might not match the spoken version. For example, the 770 * confirmation "Are you sure you want to purchase this item?" might use a visual label like 771 * "Purchase item". 772 */ 773 public static class Prompt implements Parcelable { 774 // Mandatory voice prompt. Must contain at least one item, which must not be null. 775 private final CharSequence[] mVoicePrompts; 776 777 // Mandatory visual prompt. 778 private final CharSequence mVisualPrompt; 779 780 /** 781 * Constructs a prompt set. 782 * @param voicePrompts An array of one or more voice prompts. Must not be empty or null. 783 * @param visualPrompt A prompt to display on the screen. Must not be null. 784 */ Prompt(@onNull CharSequence[] voicePrompts, @NonNull CharSequence visualPrompt)785 public Prompt(@NonNull CharSequence[] voicePrompts, @NonNull CharSequence visualPrompt) { 786 if (voicePrompts == null) { 787 throw new NullPointerException("voicePrompts must not be null"); 788 } 789 if (voicePrompts.length == 0) { 790 throw new IllegalArgumentException("voicePrompts must not be empty"); 791 } 792 if (visualPrompt == null) { 793 throw new NullPointerException("visualPrompt must not be null"); 794 } 795 this.mVoicePrompts = voicePrompts; 796 this.mVisualPrompt = visualPrompt; 797 } 798 799 /** 800 * Constructs a prompt set with single prompt used for all interactions. This is most useful 801 * in test apps. Non-trivial apps should prefer the detailed constructor. 802 */ Prompt(@onNull CharSequence prompt)803 public Prompt(@NonNull CharSequence prompt) { 804 this.mVoicePrompts = new CharSequence[] { prompt }; 805 this.mVisualPrompt = prompt; 806 } 807 808 /** 809 * Returns a prompt to use for voice interactions. 810 */ 811 @NonNull getVoicePromptAt(int index)812 public CharSequence getVoicePromptAt(int index) { 813 return mVoicePrompts[index]; 814 } 815 816 /** 817 * Returns the number of different voice prompts. 818 */ countVoicePrompts()819 public int countVoicePrompts() { 820 return mVoicePrompts.length; 821 } 822 823 /** 824 * Returns the prompt to use for visual display. 825 */ 826 @NonNull getVisualPrompt()827 public CharSequence getVisualPrompt() { 828 return mVisualPrompt; 829 } 830 831 @Override toString()832 public String toString() { 833 StringBuilder sb = new StringBuilder(128); 834 DebugUtils.buildShortClassTag(this, sb); 835 if (mVisualPrompt != null && mVoicePrompts != null && mVoicePrompts.length == 1 836 && mVisualPrompt.equals(mVoicePrompts[0])) { 837 sb.append(" "); 838 sb.append(mVisualPrompt); 839 } else { 840 if (mVisualPrompt != null) { 841 sb.append(" visual="); sb.append(mVisualPrompt); 842 } 843 if (mVoicePrompts != null) { 844 sb.append(", voice="); 845 for (int i=0; i<mVoicePrompts.length; i++) { 846 if (i > 0) sb.append(" | "); 847 sb.append(mVoicePrompts[i]); 848 } 849 } 850 } 851 sb.append('}'); 852 return sb.toString(); 853 } 854 855 /** Constructor to support Parcelable behavior. */ Prompt(Parcel in)856 Prompt(Parcel in) { 857 mVoicePrompts = in.readCharSequenceArray(); 858 mVisualPrompt = in.readCharSequence(); 859 } 860 861 @Override describeContents()862 public int describeContents() { 863 return 0; 864 } 865 866 @Override writeToParcel(Parcel dest, int flags)867 public void writeToParcel(Parcel dest, int flags) { 868 dest.writeCharSequenceArray(mVoicePrompts); 869 dest.writeCharSequence(mVisualPrompt); 870 } 871 872 public static final Creator<Prompt> CREATOR 873 = new Creator<Prompt>() { 874 public Prompt createFromParcel(Parcel in) { 875 return new Prompt(in); 876 } 877 878 public Prompt[] newArray(int size) { 879 return new Prompt[size]; 880 } 881 }; 882 } 883 VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity, Looper looper)884 VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity, 885 Looper looper) { 886 mInteractor = interactor; 887 mContext = context; 888 mActivity = activity; 889 mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true); 890 } 891 pullRequest(IVoiceInteractorRequest request, boolean complete)892 Request pullRequest(IVoiceInteractorRequest request, boolean complete) { 893 synchronized (mActiveRequests) { 894 Request req = mActiveRequests.get(request.asBinder()); 895 if (req != null && complete) { 896 mActiveRequests.remove(request.asBinder()); 897 } 898 return req; 899 } 900 } 901 makeRequestList()902 private ArrayList<Request> makeRequestList() { 903 final int N = mActiveRequests.size(); 904 if (N < 1) { 905 return null; 906 } 907 ArrayList<Request> list = new ArrayList<>(N); 908 for (int i=0; i<N; i++) { 909 list.add(mActiveRequests.valueAt(i)); 910 } 911 return list; 912 } 913 attachActivity(Activity activity)914 void attachActivity(Activity activity) { 915 mRetaining = false; 916 if (mActivity == activity) { 917 return; 918 } 919 mContext = activity; 920 mActivity = activity; 921 ArrayList<Request> reqs = makeRequestList(); 922 if (reqs != null) { 923 for (int i=0; i<reqs.size(); i++) { 924 Request req = reqs.get(i); 925 req.mContext = activity; 926 req.mActivity = activity; 927 req.onAttached(activity); 928 } 929 } 930 } 931 retainInstance()932 void retainInstance() { 933 mRetaining = true; 934 } 935 detachActivity()936 void detachActivity() { 937 ArrayList<Request> reqs = makeRequestList(); 938 if (reqs != null) { 939 for (int i=0; i<reqs.size(); i++) { 940 Request req = reqs.get(i); 941 req.onDetached(); 942 req.mActivity = null; 943 req.mContext = null; 944 } 945 } 946 if (!mRetaining) { 947 reqs = makeRequestList(); 948 if (reqs != null) { 949 for (int i=0; i<reqs.size(); i++) { 950 Request req = reqs.get(i); 951 req.cancel(); 952 } 953 } 954 mActiveRequests.clear(); 955 } 956 mContext = null; 957 mActivity = null; 958 } 959 submitRequest(Request request)960 public boolean submitRequest(Request request) { 961 return submitRequest(request, null); 962 } 963 964 /** 965 * Submit a new {@link Request} to the voice interaction service. The request must be 966 * one of the available subclasses -- {@link ConfirmationRequest}, {@link PickOptionRequest}, 967 * {@link CompleteVoiceRequest}, {@link AbortVoiceRequest}, or {@link CommandRequest}. 968 * 969 * @param request The desired request to submit. 970 * @param name An optional name for this request, or null. This can be used later with 971 * {@link #getActiveRequests} and {@link #getActiveRequest} to find the request. 972 * 973 * @return Returns true of the request was successfully submitted, else false. 974 */ submitRequest(Request request, String name)975 public boolean submitRequest(Request request, String name) { 976 try { 977 if (request.mRequestInterface != null) { 978 throw new IllegalStateException("Given " + request + " is already active"); 979 } 980 IVoiceInteractorRequest ireq = request.submit(mInteractor, 981 mContext.getOpPackageName(), mCallback); 982 request.mRequestInterface = ireq; 983 request.mContext = mContext; 984 request.mActivity = mActivity; 985 request.mName = name; 986 synchronized (mActiveRequests) { 987 mActiveRequests.put(ireq.asBinder(), request); 988 } 989 return true; 990 } catch (RemoteException e) { 991 Log.w(TAG, "Remove voice interactor service died", e); 992 return false; 993 } 994 } 995 996 /** 997 * Return all currently active requests. 998 */ getActiveRequests()999 public Request[] getActiveRequests() { 1000 synchronized (mActiveRequests) { 1001 final int N = mActiveRequests.size(); 1002 if (N <= 0) { 1003 return NO_REQUESTS; 1004 } 1005 Request[] requests = new Request[N]; 1006 for (int i=0; i<N; i++) { 1007 requests[i] = mActiveRequests.valueAt(i); 1008 } 1009 return requests; 1010 } 1011 } 1012 1013 /** 1014 * Return any currently active request that was submitted with the given name. 1015 * 1016 * @param name The name used to submit the request, as per 1017 * {@link #submitRequest(android.app.VoiceInteractor.Request, String)}. 1018 * @return Returns the active request with that name, or null if there was none. 1019 */ getActiveRequest(String name)1020 public Request getActiveRequest(String name) { 1021 synchronized (mActiveRequests) { 1022 final int N = mActiveRequests.size(); 1023 for (int i=0; i<N; i++) { 1024 Request req = mActiveRequests.valueAt(i); 1025 if (name == req.getName() || (name != null && name.equals(req.getName()))) { 1026 return req; 1027 } 1028 } 1029 } 1030 return null; 1031 } 1032 1033 /** 1034 * Queries the supported commands available from the VoiceInteractionService. 1035 * The command is a string that describes the generic operation to be performed. 1036 * An example might be "org.example.commands.PICK_DATE" to ask the user to pick 1037 * a date. (Note: This is not an actual working example.) 1038 * 1039 * @param commands The array of commands to query for support. 1040 * @return Array of booleans indicating whether each command is supported or not. 1041 */ supportsCommands(String[] commands)1042 public boolean[] supportsCommands(String[] commands) { 1043 try { 1044 boolean[] res = mInteractor.supportsCommands(mContext.getOpPackageName(), commands); 1045 if (DEBUG) Log.d(TAG, "supportsCommands: cmds=" + commands + " res=" + res); 1046 return res; 1047 } catch (RemoteException e) { 1048 throw new RuntimeException("Voice interactor has died", e); 1049 } 1050 } 1051 dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)1052 void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 1053 String innerPrefix = prefix + " "; 1054 if (mActiveRequests.size() > 0) { 1055 writer.print(prefix); writer.println("Active voice requests:"); 1056 for (int i=0; i<mActiveRequests.size(); i++) { 1057 Request req = mActiveRequests.valueAt(i); 1058 writer.print(prefix); writer.print(" #"); writer.print(i); 1059 writer.print(": "); 1060 writer.println(req); 1061 req.dump(innerPrefix, fd, writer, args); 1062 } 1063 } 1064 writer.print(prefix); writer.println("VoiceInteractor misc state:"); 1065 writer.print(prefix); writer.print(" mInteractor="); 1066 writer.println(mInteractor.asBinder()); 1067 writer.print(prefix); writer.print(" mActivity="); writer.println(mActivity); 1068 } 1069 } 1070