AlwaysOnHotwordDetector.java revision 50c290406af81850a5d876754fd2bb4bd29df5d4
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 * Creates an intent to start the un-enrollment for the associated keyphrase. 472 * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}. 473 * Starting re-enrollment is only valid if the keyphrase is already enrolled, 474 * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error. 475 * 476 * @return An {@link Intent} to start un-enrollment for the given keyphrase. 477 * @throws UnsupportedOperationException if managing they keyphrase isn't supported. 478 * Callers should only call this method after a supported state callback on 479 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 480 * @throws IllegalStateException if the detector is in an invalid state. 481 * This may happen if another detector has been instantiated or the 482 * {@link VoiceInteractionService} hosting this detector has been shut down. 483 */ 484 public Intent createUnEnrollIntent() { 485 if (DBG) Slog.d(TAG, "createUnEnrollIntent"); 486 synchronized (mLock) { 487 return getManageIntentLocked(MANAGE_ACTION_UN_ENROLL); 488 } 489 } 490 491 /** 492 * Creates an intent to start the re-enrollment for the associated keyphrase. 493 * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}. 494 * Starting re-enrollment is only valid if the keyphrase is already enrolled, 495 * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error. 496 * 497 * @return An {@link Intent} to start re-enrollment for the given keyphrase. 498 * @throws UnsupportedOperationException if managing they keyphrase isn't supported. 499 * Callers should only call this method after a supported state callback on 500 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 501 * @throws IllegalStateException if the detector is in an invalid state. 502 * This may happen if another detector has been instantiated or the 503 * {@link VoiceInteractionService} hosting this detector has been shut down. 504 */ 505 public Intent createReEnrollIntent() { 506 if (DBG) Slog.d(TAG, "createReEnrollIntent"); 507 synchronized (mLock) { 508 return getManageIntentLocked(MANAGE_ACTION_RE_ENROLL); 509 } 510 } 511 512 private Intent getManageIntentLocked(int action) { 513 if (mAvailability == STATE_INVALID) { 514 throw new IllegalStateException("getManageIntent called on an invalid detector"); 515 } 516 517 // This method only makes sense if we can actually support a recognition. 518 if (mAvailability != STATE_KEYPHRASE_ENROLLED 519 && mAvailability != STATE_KEYPHRASE_UNENROLLED) { 520 throw new UnsupportedOperationException( 521 "Managing the given keyphrase is not supported"); 522 } 523 524 return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale); 525 } 526 527 /** 528 * Invalidates this hotword detector so that any future calls to this result 529 * in an IllegalStateException. 530 * 531 * @hide 532 */ 533 void invalidate() { 534 synchronized (mLock) { 535 mAvailability = STATE_INVALID; 536 notifyStateChangedLocked(); 537 } 538 } 539 540 /** 541 * Reloads the sound models from the service. 542 * 543 * @hide 544 */ 545 void onSoundModelsChanged() { 546 synchronized (mLock) { 547 if (mAvailability == STATE_INVALID 548 || mAvailability == STATE_HARDWARE_UNAVAILABLE 549 || mAvailability == STATE_KEYPHRASE_UNSUPPORTED) { 550 Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config"); 551 return; 552 } 553 554 // Stop the recognition before proceeding. 555 // This is done because we want to stop the recognition on an older model if it changed 556 // or was deleted. 557 // The availability change callback should ensure that the client starts recognition 558 // again if needed. 559 stopRecognitionLocked(); 560 561 // Execute a refresh availability task - which should then notify of a change. 562 new RefreshAvailabiltyTask().execute(); 563 } 564 } 565 566 private int startRecognitionLocked(int recognitionFlags) { 567 KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1]; 568 // TODO: Do we need to do something about the confidence level here? 569 recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.id, 570 mKeyphraseMetadata.recognitionModeFlags, 0, new ConfidenceLevel[0]); 571 boolean captureTriggerAudio = 572 (recognitionFlags&RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0; 573 boolean allowMultipleTriggers = 574 (recognitionFlags&RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0; 575 int code = STATUS_ERROR; 576 try { 577 code = mModelManagementService.startRecognition(mVoiceInteractionService, 578 mKeyphraseMetadata.id, mInternalCallback, 579 new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers, 580 recognitionExtra, null /* additional data */)); 581 } catch (RemoteException e) { 582 Slog.w(TAG, "RemoteException in startRecognition!", e); 583 } 584 if (code != STATUS_OK) { 585 Slog.w(TAG, "startRecognition() failed with error code " + code); 586 } 587 return code; 588 } 589 590 private int stopRecognitionLocked() { 591 int code = STATUS_ERROR; 592 try { 593 code = mModelManagementService.stopRecognition( 594 mVoiceInteractionService, mKeyphraseMetadata.id, mInternalCallback); 595 } catch (RemoteException e) { 596 Slog.w(TAG, "RemoteException in stopRecognition!", e); 597 } 598 599 if (code != STATUS_OK) { 600 Slog.w(TAG, "stopRecognition() failed with error code " + code); 601 } 602 return code; 603 } 604 605 private void notifyStateChangedLocked() { 606 Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED); 607 message.arg1 = mAvailability; 608 message.sendToTarget(); 609 } 610 611 /** @hide */ 612 static final class SoundTriggerListener extends IRecognitionStatusCallback.Stub { 613 private final Handler mHandler; 614 615 public SoundTriggerListener(Handler handler) { 616 mHandler = handler; 617 } 618 619 @Override 620 public void onDetected(KeyphraseRecognitionEvent event) { 621 if (DBG) { 622 Slog.d(TAG, "onDetected(" + event + ")"); 623 } else { 624 Slog.i(TAG, "onDetected"); 625 } 626 Message.obtain(mHandler, MSG_HOTWORD_DETECTED, 627 new EventPayload(event.triggerInData, event.captureAvailable, 628 event.captureFormat, event.captureSession, event.data)) 629 .sendToTarget(); 630 } 631 632 @Override 633 public void onError(int status) { 634 Slog.i(TAG, "onError: " + status); 635 mHandler.sendEmptyMessage(MSG_DETECTION_ERROR); 636 } 637 638 @Override 639 public void onRecognitionPaused() { 640 Slog.i(TAG, "onRecognitionPaused"); 641 mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE); 642 } 643 644 @Override 645 public void onRecognitionResumed() { 646 Slog.i(TAG, "onRecognitionResumed"); 647 mHandler.sendEmptyMessage(MSG_DETECTION_RESUME); 648 } 649 } 650 651 class MyHandler extends Handler { 652 @Override 653 public void handleMessage(Message msg) { 654 synchronized (mLock) { 655 if (mAvailability == STATE_INVALID) { 656 Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector"); 657 return; 658 } 659 } 660 661 switch (msg.what) { 662 case MSG_AVAILABILITY_CHANGED: 663 mExternalCallback.onAvailabilityChanged(msg.arg1); 664 break; 665 case MSG_HOTWORD_DETECTED: 666 mExternalCallback.onDetected((EventPayload) msg.obj); 667 break; 668 case MSG_DETECTION_ERROR: 669 mExternalCallback.onError(); 670 break; 671 case MSG_DETECTION_PAUSE: 672 mExternalCallback.onRecognitionPaused(); 673 break; 674 case MSG_DETECTION_RESUME: 675 mExternalCallback.onRecognitionResumed(); 676 break; 677 default: 678 super.handleMessage(msg); 679 } 680 } 681 } 682 683 class RefreshAvailabiltyTask extends AsyncTask<Void, Void, Void> { 684 685 @Override 686 public Void doInBackground(Void... params) { 687 int availability = internalGetInitialAvailability(); 688 boolean enrolled = false; 689 // Fetch the sound model if the availability is one of the supported ones. 690 if (availability == STATE_NOT_READY 691 || availability == STATE_KEYPHRASE_UNENROLLED 692 || availability == STATE_KEYPHRASE_ENROLLED) { 693 enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id); 694 if (!enrolled) { 695 availability = STATE_KEYPHRASE_UNENROLLED; 696 } else { 697 availability = STATE_KEYPHRASE_ENROLLED; 698 } 699 } 700 701 synchronized (mLock) { 702 if (DBG) { 703 Slog.d(TAG, "Hotword availability changed from " + mAvailability 704 + " -> " + availability); 705 } 706 mAvailability = availability; 707 notifyStateChangedLocked(); 708 } 709 return null; 710 } 711 712 /** 713 * @return The initial availability without checking the enrollment status. 714 */ 715 private int internalGetInitialAvailability() { 716 synchronized (mLock) { 717 // This detector has already been invalidated. 718 if (mAvailability == STATE_INVALID) { 719 return STATE_INVALID; 720 } 721 } 722 723 ModuleProperties dspModuleProperties = null; 724 try { 725 dspModuleProperties = 726 mModelManagementService.getDspModuleProperties(mVoiceInteractionService); 727 } catch (RemoteException e) { 728 Slog.w(TAG, "RemoteException in getDspProperties!", e); 729 } 730 // No DSP available 731 if (dspModuleProperties == null) { 732 return STATE_HARDWARE_UNAVAILABLE; 733 } 734 // No enrollment application supports this keyphrase/locale 735 if (mKeyphraseMetadata == null) { 736 return STATE_KEYPHRASE_UNSUPPORTED; 737 } 738 return STATE_NOT_READY; 739 } 740 741 /** 742 * @return The corresponding {@link KeyphraseSoundModel} or null if none is found. 743 */ 744 private boolean internalGetIsEnrolled(int keyphraseId) { 745 try { 746 return mModelManagementService.isEnrolledForKeyphrase( 747 mVoiceInteractionService, keyphraseId); 748 } catch (RemoteException e) { 749 Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!", e); 750 } 751 return false; 752 } 753 } 754 755 /** @hide */ 756 public void dump(String prefix, PrintWriter pw) { 757 synchronized (mLock) { 758 pw.print(prefix); pw.print("Text="); pw.println(mText); 759 pw.print(prefix); pw.print("Locale="); pw.println(mLocale); 760 pw.print(prefix); pw.print("Availability="); pw.println(mAvailability); 761 pw.print(prefix); pw.print("KeyphraseMetadata="); pw.println(mKeyphraseMetadata); 762 pw.print(prefix); pw.print("EnrollmentInfo="); pw.println(mKeyphraseEnrollmentInfo); 763 } 764 } 765} 766