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