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