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