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