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