AlwaysOnHotwordDetector.java revision ae686a51288031271685861436f7c67201791d09
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 17package android.service.voice; 18 19import android.annotation.IntDef; 20import android.annotation.NonNull; 21import android.annotation.Nullable; 22import android.app.Activity; 23import android.content.Intent; 24import android.hardware.soundtrigger.IRecognitionStatusCallback; 25import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; 26import android.hardware.soundtrigger.KeyphraseMetadata; 27import android.hardware.soundtrigger.SoundTrigger; 28import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel; 29import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent; 30import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra; 31import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; 32import android.hardware.soundtrigger.SoundTrigger.ModuleProperties; 33import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; 34import android.media.AudioFormat; 35import android.os.AsyncTask; 36import android.os.Handler; 37import android.os.Message; 38import android.os.RemoteException; 39import android.util.Slog; 40 41import com.android.internal.app.IVoiceInteractionManagerService; 42 43import java.io.PrintWriter; 44import java.lang.annotation.Retention; 45import java.lang.annotation.RetentionPolicy; 46import java.util.Locale; 47 48/** 49 * A class that lets a VoiceInteractionService implementation interact with 50 * always-on keyphrase detection APIs. 51 */ 52public class AlwaysOnHotwordDetector { 53 //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----// 54 /** 55 * Indicates that this hotword detector is no longer valid for any recognition 56 * and should not be used anymore. 57 */ 58 private static final int STATE_INVALID = -3; 59 60 /** 61 * Indicates that recognition for the given keyphrase is not available on the system 62 * because of the hardware configuration. 63 * No further interaction should be performed with the detector that returns this availability. 64 */ 65 public static final int STATE_HARDWARE_UNAVAILABLE = -2; 66 /** 67 * Indicates that recognition for the given keyphrase is not supported. 68 * No further interaction should be performed with the detector that returns this availability. 69 */ 70 public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; 71 /** 72 * Indicates that the given keyphrase is not enrolled. 73 * The caller may choose to begin an enrollment flow for the keyphrase. 74 */ 75 public static final int STATE_KEYPHRASE_UNENROLLED = 1; 76 /** 77 * Indicates that the given keyphrase is currently enrolled and it's possible to start 78 * recognition for it. 79 */ 80 public static final int STATE_KEYPHRASE_ENROLLED = 2; 81 82 /** 83 * Indicates that the detector isn't ready currently. 84 */ 85 private static final int STATE_NOT_READY = 0; 86 87 // Keyphrase management actions. Used in getManageIntent() ----// 88 @Retention(RetentionPolicy.SOURCE) 89 @IntDef(value = { 90 MANAGE_ACTION_ENROLL, 91 MANAGE_ACTION_RE_ENROLL, 92 MANAGE_ACTION_UN_ENROLL 93 }) 94 private @interface ManageActions {} 95 96 /** 97 * Indicates that we need to enroll. 98 * 99 * @hide 100 */ 101 public static final int MANAGE_ACTION_ENROLL = 0; 102 /** 103 * Indicates that we need to re-enroll. 104 * 105 * @hide 106 */ 107 public static final int MANAGE_ACTION_RE_ENROLL = 1; 108 /** 109 * Indicates that we need to un-enroll. 110 * 111 * @hide 112 */ 113 public static final int MANAGE_ACTION_UN_ENROLL = 2; 114 115 //-- Flags for startRecognition ----// 116 /** @hide */ 117 @Retention(RetentionPolicy.SOURCE) 118 @IntDef(flag = true, 119 value = { 120 RECOGNITION_FLAG_NONE, 121 RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO, 122 RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS 123 }) 124 public @interface RecognitionFlags {} 125 126 /** 127 * Empty flag for {@link #startRecognition(int)}. 128 * 129 * @hide 130 */ 131 public static final int RECOGNITION_FLAG_NONE = 0; 132 /** 133 * Recognition flag for {@link #startRecognition(int)} that indicates 134 * whether the trigger audio for hotword needs to be captured. 135 */ 136 public static final int RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO = 0x1; 137 /** 138 * Recognition flag for {@link #startRecognition(int)} that indicates 139 * whether the recognition should keep going on even after the keyphrase triggers. 140 * If this flag is specified, it's possible to get multiple triggers after a 141 * call to {@link #startRecognition(int)} if the user speaks the keyphrase multiple times. 142 * When this isn't specified, the default behavior is to stop recognition once the 143 * keyphrase is spoken, till the caller starts recognition again. 144 */ 145 public static final int RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS = 0x2; 146 147 //---- Recognition mode flags. Return codes for getSupportedRecognitionModes() ----// 148 // Must be kept in sync with the related attribute defined as searchKeyphraseRecognitionFlags. 149 150 /** @hide */ 151 @Retention(RetentionPolicy.SOURCE) 152 @IntDef(flag = true, 153 value = { 154 RECOGNITION_MODE_VOICE_TRIGGER, 155 RECOGNITION_MODE_USER_IDENTIFICATION, 156 }) 157 public @interface RecognitionModes {} 158 159 /** 160 * Simple recognition of the key phrase. 161 * Returned by {@link #getSupportedRecognitionModes()} 162 */ 163 public static final int RECOGNITION_MODE_VOICE_TRIGGER 164 = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER; 165 /** 166 * User identification performed with the keyphrase recognition. 167 * Returned by {@link #getSupportedRecognitionModes()} 168 */ 169 public static final int RECOGNITION_MODE_USER_IDENTIFICATION 170 = SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION; 171 172 static final String TAG = "AlwaysOnHotwordDetector"; 173 // TODO: Set to false. 174 static final boolean DBG = true; 175 176 private static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR; 177 private static final int STATUS_OK = SoundTrigger.STATUS_OK; 178 179 private static final int MSG_AVAILABILITY_CHANGED = 1; 180 private static final int MSG_HOTWORD_DETECTED = 2; 181 private static final int MSG_DETECTION_ERROR = 3; 182 private static final int MSG_DETECTION_PAUSE = 4; 183 private static final int MSG_DETECTION_RESUME = 5; 184 185 private final String mText; 186 private final Locale mLocale; 187 /** 188 * The metadata of the Keyphrase, derived from the enrollment application. 189 * This may be null if this keyphrase isn't supported by the enrollment application. 190 */ 191 private final KeyphraseMetadata mKeyphraseMetadata; 192 private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; 193 private final IVoiceInteractionService mVoiceInteractionService; 194 private final IVoiceInteractionManagerService mModelManagementService; 195 private final SoundTriggerListener mInternalCallback; 196 private final Callback mExternalCallback; 197 private final Object mLock = new Object(); 198 private final Handler mHandler; 199 200 private int mAvailability = STATE_NOT_READY; 201 202 /** 203 * Additional payload for {@link Callback#onDetected}. 204 */ 205 public static class EventPayload { 206 private final boolean mTriggerAvailable; 207 // Indicates if {@code captureSession} can be used to continue capturing more audio 208 // from the DSP hardware. 209 private final boolean mCaptureAvailable; 210 // The session to use when attempting to capture more audio from the DSP hardware. 211 private final int mCaptureSession; 212 private final AudioFormat mAudioFormat; 213 // Raw data associated with the event. 214 // This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true. 215 private final byte[] mData; 216 217 private EventPayload(boolean triggerAvailable, boolean captureAvailable, 218 AudioFormat audioFormat, int captureSession, byte[] data) { 219 mTriggerAvailable = triggerAvailable; 220 mCaptureAvailable = captureAvailable; 221 mCaptureSession = captureSession; 222 mAudioFormat = audioFormat; 223 mData = data; 224 } 225 226 /** 227 * Gets the format of the audio obtained using {@link #getTriggerAudio()}. 228 * May be null if there's no audio present. 229 */ 230 @Nullable 231 public AudioFormat getCaptureAudioFormat() { 232 return mAudioFormat; 233 } 234 235 /** 236 * Gets the raw audio that triggered the keyphrase. 237 * This may be null if the trigger audio isn't available. 238 * If non-null, the format of the audio can be obtained by calling 239 * {@link #getCaptureAudioFormat()}. 240 * 241 * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO 242 */ 243 @Nullable 244 public byte[] getTriggerAudio() { 245 if (mTriggerAvailable) { 246 return mData; 247 } else { 248 return null; 249 } 250 } 251 252 /** 253 * Gets the session ID to start a capture from the DSP. 254 * This may be null if streaming capture isn't possible. 255 * If non-null, the format of the audio that can be captured can be 256 * obtained using {@link #getCaptureAudioFormat()}. 257 * 258 * TODO: Candidate for Public API when the API to start capture with a session ID 259 * is made public. 260 * 261 * TODO: Add this to {@link #getCaptureAudioFormat()}: 262 * "Gets the format of the audio obtained using {@link #getTriggerAudio()} 263 * or {@link #getCaptureSession()}. May be null if no audio can be obtained 264 * for either the trigger or a streaming session." 265 * 266 * TODO: Should this return a known invalid value instead? 267 * 268 * @hide 269 */ 270 @Nullable 271 public Integer getCaptureSession() { 272 if (mCaptureAvailable) { 273 return mCaptureSession; 274 } else { 275 return null; 276 } 277 } 278 } 279 280 /** 281 * Callbacks for always-on hotword detection. 282 */ 283 public static abstract class Callback { 284 /** 285 * Called when the hotword availability changes. 286 * This indicates a change in the availability of recognition for the given keyphrase. 287 * It's called at least once with the initial availability.<p/> 288 * 289 * Availability implies whether the hardware on this system is capable of listening for 290 * the given keyphrase or not. <p/> 291 * 292 * @see AlwaysOnHotwordDetector#STATE_HARDWARE_UNAVAILABLE 293 * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNSUPPORTED 294 * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNENROLLED 295 * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_ENROLLED 296 */ 297 public abstract void onAvailabilityChanged(int status); 298 /** 299 * Called when the keyphrase is spoken. 300 * This implicitly stops listening for the keyphrase once it's detected. 301 * Clients should start a recognition again once they are done handling this 302 * detection. 303 * 304 * @param eventPayload Payload data for the detection event. 305 * This may contain the trigger audio, if requested when calling 306 * {@link AlwaysOnHotwordDetector#startRecognition(int)}. 307 */ 308 public abstract void onDetected(@NonNull EventPayload eventPayload); 309 /** 310 * Called when the detection fails due to an error. 311 */ 312 public abstract void onError(); 313 /** 314 * Called when the recognition is paused temporarily for some reason. 315 * This is an informational callback, and the clients shouldn't be doing anything here 316 * except showing an indication on their UI if they have to. 317 */ 318 public abstract void onRecognitionPaused(); 319 /** 320 * Called when the recognition is resumed after it was temporarily paused. 321 * This is an informational callback, and the clients shouldn't be doing anything here 322 * except showing an indication on their UI if they have to. 323 */ 324 public abstract void onRecognitionResumed(); 325 } 326 327 /** 328 * @param text The keyphrase text to get the detector for. 329 * @param locale The java locale for the detector. 330 * @param callback A non-null Callback for receiving the recognition events. 331 * @param voiceInteractionService The current voice interaction service. 332 * @param modelManagementService A service that allows management of sound models. 333 * 334 * @hide 335 */ 336 public AlwaysOnHotwordDetector(String text, Locale locale, Callback callback, 337 KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, 338 IVoiceInteractionService voiceInteractionService, 339 IVoiceInteractionManagerService modelManagementService) { 340 mText = text; 341 mLocale = locale; 342 mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo; 343 mKeyphraseMetadata = mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale); 344 mExternalCallback = callback; 345 mHandler = new MyHandler(); 346 mInternalCallback = new SoundTriggerListener(mHandler); 347 mVoiceInteractionService = voiceInteractionService; 348 mModelManagementService = modelManagementService; 349 new RefreshAvailabiltyTask().execute(); 350 } 351 352 /** 353 * Gets the recognition modes supported by the associated keyphrase. 354 * 355 * @see #RECOGNITION_MODE_USER_IDENTIFICATION 356 * @see #RECOGNITION_MODE_VOICE_TRIGGER 357 * 358 * @throws UnsupportedOperationException if the keyphrase itself isn't supported. 359 * Callers should only call this method after a supported state callback on 360 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 361 * @throws IllegalStateException if the detector is in an invalid state. 362 * This may happen if another detector has been instantiated or the 363 * {@link VoiceInteractionService} hosting this detector has been shut down. 364 */ 365 public @RecognitionModes int getSupportedRecognitionModes() { 366 if (DBG) Slog.d(TAG, "getSupportedRecognitionModes()"); 367 synchronized (mLock) { 368 return getSupportedRecognitionModesLocked(); 369 } 370 } 371 372 private int getSupportedRecognitionModesLocked() { 373 if (mAvailability == STATE_INVALID) { 374 throw new IllegalStateException( 375 "getSupportedRecognitionModes called on an invalid detector"); 376 } 377 378 // This method only makes sense if we can actually support a recognition. 379 if (mAvailability != STATE_KEYPHRASE_ENROLLED 380 && mAvailability != STATE_KEYPHRASE_UNENROLLED) { 381 throw new UnsupportedOperationException( 382 "Getting supported recognition modes for the keyphrase is not supported"); 383 } 384 385 return mKeyphraseMetadata.recognitionModeFlags; 386 } 387 388 /** 389 * Starts recognition for the associated keyphrase. 390 * 391 * @see #RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO 392 * @see #RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS 393 * 394 * @param recognitionFlags The flags to control the recognition properties. 395 * @return Indicates whether the call succeeded or not. 396 * @throws UnsupportedOperationException if the recognition isn't supported. 397 * Callers should only call this method after a supported state callback on 398 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 399 * @throws IllegalStateException if the detector is in an invalid state. 400 * This may happen if another detector has been instantiated or the 401 * {@link VoiceInteractionService} hosting this detector has been shut down. 402 */ 403 public boolean startRecognition(@RecognitionFlags int recognitionFlags) { 404 if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")"); 405 synchronized (mLock) { 406 if (mAvailability == STATE_INVALID) { 407 throw new IllegalStateException("startRecognition called on an invalid detector"); 408 } 409 410 // Check if we can start/stop a recognition. 411 if (mAvailability != STATE_KEYPHRASE_ENROLLED) { 412 throw new UnsupportedOperationException( 413 "Recognition for the given keyphrase is not supported"); 414 } 415 416 return startRecognitionLocked(recognitionFlags) == STATUS_OK; 417 } 418 } 419 420 /** 421 * Stops recognition for the associated keyphrase. 422 * 423 * @return Indicates whether the call succeeded or not. 424 * @throws UnsupportedOperationException if the recognition isn't supported. 425 * Callers should only call this method after a supported state callback on 426 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 427 * @throws IllegalStateException if the detector is in an invalid state. 428 * This may happen if another detector has been instantiated or the 429 * {@link VoiceInteractionService} hosting this detector has been shut down. 430 */ 431 public boolean stopRecognition() { 432 if (DBG) Slog.d(TAG, "stopRecognition()"); 433 synchronized (mLock) { 434 if (mAvailability == STATE_INVALID) { 435 throw new IllegalStateException("stopRecognition called on an invalid detector"); 436 } 437 438 // Check if we can start/stop a recognition. 439 if (mAvailability != STATE_KEYPHRASE_ENROLLED) { 440 throw new UnsupportedOperationException( 441 "Recognition for the given keyphrase is not supported"); 442 } 443 444 return stopRecognitionLocked() == STATUS_OK; 445 } 446 } 447 448 /** 449 * Creates an intent to start the enrollment for the associated keyphrase. 450 * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}. 451 * Starting re-enrollment is only valid if the keyphrase is un-enrolled, 452 * i.e. {@link #STATE_KEYPHRASE_UNENROLLED}, 453 * otherwise {@link #createReEnrollIntent()} should be preferred. 454 * 455 * @return An {@link Intent} to start enrollment for the given keyphrase. 456 * @throws UnsupportedOperationException if managing they keyphrase isn't supported. 457 * Callers should only call this method after a supported state callback on 458 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 459 * @throws IllegalStateException if the detector is in an invalid state. 460 * This may happen if another detector has been instantiated or the 461 * {@link VoiceInteractionService} hosting this detector has been shut down. 462 */ 463 public Intent createEnrollIntent() { 464 if (DBG) Slog.d(TAG, "createEnrollIntent"); 465 synchronized (mLock) { 466 return getManageIntentLocked(MANAGE_ACTION_ENROLL); 467 } 468 } 469 470 /** 471 * FIXME: Remove once the prebuilts are updated. 472 * 473 * @hide 474 */ 475 @Deprecated 476 public Intent createIntentToEnroll() { 477 if (DBG) Slog.d(TAG, "createIntentToEnroll"); 478 synchronized (mLock) { 479 return getManageIntentLocked(MANAGE_ACTION_ENROLL); 480 } 481 } 482 483 /** 484 * Creates an intent to start the un-enrollment for the associated keyphrase. 485 * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}. 486 * Starting re-enrollment is only valid if the keyphrase is already enrolled, 487 * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error. 488 * 489 * @return An {@link Intent} to start un-enrollment for the given keyphrase. 490 * @throws UnsupportedOperationException if managing they keyphrase isn't supported. 491 * Callers should only call this method after a supported state callback on 492 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 493 * @throws IllegalStateException if the detector is in an invalid state. 494 * This may happen if another detector has been instantiated or the 495 * {@link VoiceInteractionService} hosting this detector has been shut down. 496 */ 497 public Intent createUnEnrollIntent() { 498 if (DBG) Slog.d(TAG, "createUnEnrollIntent"); 499 synchronized (mLock) { 500 return getManageIntentLocked(MANAGE_ACTION_UN_ENROLL); 501 } 502 } 503 504 /** 505 * FIXME: Remove once the prebuilts are updated. 506 * 507 * @hide 508 */ 509 @Deprecated 510 public Intent createIntentToUnEnroll() { 511 if (DBG) Slog.d(TAG, "createIntentToUnEnroll"); 512 synchronized (mLock) { 513 return getManageIntentLocked(MANAGE_ACTION_UN_ENROLL); 514 } 515 } 516 517 /** 518 * Creates an intent to start the re-enrollment for the associated keyphrase. 519 * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}. 520 * Starting re-enrollment is only valid if the keyphrase is already enrolled, 521 * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error. 522 * 523 * @return An {@link Intent} to start re-enrollment for the given keyphrase. 524 * @throws UnsupportedOperationException if managing they keyphrase isn't supported. 525 * Callers should only call this method after a supported state callback on 526 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 527 * @throws IllegalStateException if the detector is in an invalid state. 528 * This may happen if another detector has been instantiated or the 529 * {@link VoiceInteractionService} hosting this detector has been shut down. 530 */ 531 public Intent createReEnrollIntent() { 532 if (DBG) Slog.d(TAG, "createReEnrollIntent"); 533 synchronized (mLock) { 534 return getManageIntentLocked(MANAGE_ACTION_RE_ENROLL); 535 } 536 } 537 538 /** 539 * FIXME: Remove once the prebuilts are updated. 540 * 541 * @hide 542 */ 543 @Deprecated 544 public Intent createIntentToReEnroll() { 545 if (DBG) Slog.d(TAG, "createIntentToReEnroll"); 546 synchronized (mLock) { 547 return getManageIntentLocked(MANAGE_ACTION_RE_ENROLL); 548 } 549 } 550 551 private Intent getManageIntentLocked(int action) { 552 if (mAvailability == STATE_INVALID) { 553 throw new IllegalStateException("getManageIntent called on an invalid detector"); 554 } 555 556 // This method only makes sense if we can actually support a recognition. 557 if (mAvailability != STATE_KEYPHRASE_ENROLLED 558 && mAvailability != STATE_KEYPHRASE_UNENROLLED) { 559 throw new UnsupportedOperationException( 560 "Managing the given keyphrase is not supported"); 561 } 562 563 return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale); 564 } 565 566 /** 567 * Invalidates this hotword detector so that any future calls to this result 568 * in an IllegalStateException. 569 * 570 * @hide 571 */ 572 void invalidate() { 573 synchronized (mLock) { 574 mAvailability = STATE_INVALID; 575 notifyStateChangedLocked(); 576 } 577 } 578 579 /** 580 * Reloads the sound models from the service. 581 * 582 * @hide 583 */ 584 void onSoundModelsChanged() { 585 synchronized (mLock) { 586 if (mAvailability == STATE_INVALID 587 || mAvailability == STATE_HARDWARE_UNAVAILABLE 588 || mAvailability == STATE_KEYPHRASE_UNSUPPORTED) { 589 Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config"); 590 return; 591 } 592 593 // Stop the recognition before proceeding. 594 // This is done because we want to stop the recognition on an older model if it changed 595 // or was deleted. 596 // The availability change callback should ensure that the client starts recognition 597 // again if needed. 598 stopRecognitionLocked(); 599 600 // Execute a refresh availability task - which should then notify of a change. 601 new RefreshAvailabiltyTask().execute(); 602 } 603 } 604 605 private int startRecognitionLocked(int recognitionFlags) { 606 KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1]; 607 // TODO: Do we need to do something about the confidence level here? 608 recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.id, 609 mKeyphraseMetadata.recognitionModeFlags, 0, new ConfidenceLevel[0]); 610 boolean captureTriggerAudio = 611 (recognitionFlags&RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0; 612 boolean allowMultipleTriggers = 613 (recognitionFlags&RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0; 614 int code = STATUS_ERROR; 615 try { 616 code = mModelManagementService.startRecognition(mVoiceInteractionService, 617 mKeyphraseMetadata.id, mInternalCallback, 618 new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers, 619 recognitionExtra, null /* additional data */)); 620 } catch (RemoteException e) { 621 Slog.w(TAG, "RemoteException in startRecognition!", e); 622 } 623 if (code != STATUS_OK) { 624 Slog.w(TAG, "startRecognition() failed with error code " + code); 625 } 626 return code; 627 } 628 629 private int stopRecognitionLocked() { 630 int code = STATUS_ERROR; 631 try { 632 code = mModelManagementService.stopRecognition( 633 mVoiceInteractionService, mKeyphraseMetadata.id, mInternalCallback); 634 } catch (RemoteException e) { 635 Slog.w(TAG, "RemoteException in stopRecognition!", e); 636 } 637 638 if (code != STATUS_OK) { 639 Slog.w(TAG, "stopRecognition() failed with error code " + code); 640 } 641 return code; 642 } 643 644 private void notifyStateChangedLocked() { 645 Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED); 646 message.arg1 = mAvailability; 647 message.sendToTarget(); 648 } 649 650 /** @hide */ 651 static final class SoundTriggerListener extends IRecognitionStatusCallback.Stub { 652 private final Handler mHandler; 653 654 public SoundTriggerListener(Handler handler) { 655 mHandler = handler; 656 } 657 658 @Override 659 public void onDetected(KeyphraseRecognitionEvent event) { 660 if (DBG) { 661 Slog.d(TAG, "onDetected(" + event + ")"); 662 } else { 663 Slog.i(TAG, "onDetected"); 664 } 665 Message.obtain(mHandler, MSG_HOTWORD_DETECTED, 666 new EventPayload(event.triggerInData, event.captureAvailable, 667 event.captureFormat, event.captureSession, event.data)) 668 .sendToTarget(); 669 } 670 671 @Override 672 public void onError(int status) { 673 Slog.i(TAG, "onError: " + status); 674 mHandler.sendEmptyMessage(MSG_DETECTION_ERROR); 675 } 676 677 @Override 678 public void onRecognitionPaused() { 679 Slog.i(TAG, "onRecognitionPaused"); 680 mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE); 681 } 682 683 @Override 684 public void onRecognitionResumed() { 685 Slog.i(TAG, "onRecognitionResumed"); 686 mHandler.sendEmptyMessage(MSG_DETECTION_RESUME); 687 } 688 } 689 690 class MyHandler extends Handler { 691 @Override 692 public void handleMessage(Message msg) { 693 synchronized (mLock) { 694 if (mAvailability == STATE_INVALID) { 695 Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector"); 696 return; 697 } 698 } 699 700 switch (msg.what) { 701 case MSG_AVAILABILITY_CHANGED: 702 mExternalCallback.onAvailabilityChanged(msg.arg1); 703 break; 704 case MSG_HOTWORD_DETECTED: 705 mExternalCallback.onDetected((EventPayload) msg.obj); 706 break; 707 case MSG_DETECTION_ERROR: 708 mExternalCallback.onError(); 709 break; 710 case MSG_DETECTION_PAUSE: 711 mExternalCallback.onRecognitionPaused(); 712 break; 713 case MSG_DETECTION_RESUME: 714 mExternalCallback.onRecognitionResumed(); 715 break; 716 default: 717 super.handleMessage(msg); 718 } 719 } 720 } 721 722 class RefreshAvailabiltyTask extends AsyncTask<Void, Void, Void> { 723 724 @Override 725 public Void doInBackground(Void... params) { 726 int availability = internalGetInitialAvailability(); 727 boolean enrolled = false; 728 // Fetch the sound model if the availability is one of the supported ones. 729 if (availability == STATE_NOT_READY 730 || availability == STATE_KEYPHRASE_UNENROLLED 731 || availability == STATE_KEYPHRASE_ENROLLED) { 732 enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id); 733 if (!enrolled) { 734 availability = STATE_KEYPHRASE_UNENROLLED; 735 } else { 736 availability = STATE_KEYPHRASE_ENROLLED; 737 } 738 } 739 740 synchronized (mLock) { 741 if (DBG) { 742 Slog.d(TAG, "Hotword availability changed from " + mAvailability 743 + " -> " + availability); 744 } 745 mAvailability = availability; 746 notifyStateChangedLocked(); 747 } 748 return null; 749 } 750 751 /** 752 * @return The initial availability without checking the enrollment status. 753 */ 754 private int internalGetInitialAvailability() { 755 synchronized (mLock) { 756 // This detector has already been invalidated. 757 if (mAvailability == STATE_INVALID) { 758 return STATE_INVALID; 759 } 760 } 761 762 ModuleProperties dspModuleProperties = null; 763 try { 764 dspModuleProperties = 765 mModelManagementService.getDspModuleProperties(mVoiceInteractionService); 766 } catch (RemoteException e) { 767 Slog.w(TAG, "RemoteException in getDspProperties!", e); 768 } 769 // No DSP available 770 if (dspModuleProperties == null) { 771 return STATE_HARDWARE_UNAVAILABLE; 772 } 773 // No enrollment application supports this keyphrase/locale 774 if (mKeyphraseMetadata == null) { 775 return STATE_KEYPHRASE_UNSUPPORTED; 776 } 777 return STATE_NOT_READY; 778 } 779 780 /** 781 * @return The corresponding {@link KeyphraseSoundModel} or null if none is found. 782 */ 783 private boolean internalGetIsEnrolled(int keyphraseId) { 784 try { 785 return mModelManagementService.isEnrolledForKeyphrase( 786 mVoiceInteractionService, keyphraseId); 787 } catch (RemoteException e) { 788 Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!", e); 789 } 790 return false; 791 } 792 } 793 794 /** @hide */ 795 public void dump(String prefix, PrintWriter pw) { 796 synchronized (mLock) { 797 pw.print(prefix); pw.print("Text="); pw.println(mText); 798 pw.print(prefix); pw.print("Locale="); pw.println(mLocale); 799 pw.print(prefix); pw.print("Availability="); pw.println(mAvailability); 800 pw.print(prefix); pw.print("KeyphraseMetadata="); pw.println(mKeyphraseMetadata); 801 pw.print(prefix); pw.print("EnrollmentInfo="); pw.println(mKeyphraseEnrollmentInfo); 802 } 803 } 804} 805