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