1 /** 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.service.voice; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.app.Activity; 23 import android.content.Intent; 24 import android.hardware.soundtrigger.IRecognitionStatusCallback; 25 import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; 26 import android.hardware.soundtrigger.KeyphraseMetadata; 27 import android.hardware.soundtrigger.SoundTrigger; 28 import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel; 29 import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent; 30 import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra; 31 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; 32 import android.hardware.soundtrigger.SoundTrigger.ModuleProperties; 33 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; 34 import android.media.AudioFormat; 35 import android.os.AsyncTask; 36 import android.os.Handler; 37 import android.os.Message; 38 import android.os.RemoteException; 39 import android.util.Slog; 40 41 import com.android.internal.app.IVoiceInteractionManagerService; 42 43 import java.io.PrintWriter; 44 import java.lang.annotation.Retention; 45 import java.lang.annotation.RetentionPolicy; 46 import java.util.Locale; 47 48 /** 49 * A class that lets a VoiceInteractionService implementation interact with 50 * always-on keyphrase detection APIs. 51 */ 52 public 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 static final boolean DBG = false; 174 175 private static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR; 176 private static final int STATUS_OK = SoundTrigger.STATUS_OK; 177 178 private static final int MSG_AVAILABILITY_CHANGED = 1; 179 private static final int MSG_HOTWORD_DETECTED = 2; 180 private static final int MSG_DETECTION_ERROR = 3; 181 private static final int MSG_DETECTION_PAUSE = 4; 182 private static final int MSG_DETECTION_RESUME = 5; 183 184 private final String mText; 185 private final Locale mLocale; 186 /** 187 * The metadata of the Keyphrase, derived from the enrollment application. 188 * This may be null if this keyphrase isn't supported by the enrollment application. 189 */ 190 private final KeyphraseMetadata mKeyphraseMetadata; 191 private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; 192 private final IVoiceInteractionService mVoiceInteractionService; 193 private final IVoiceInteractionManagerService mModelManagementService; 194 private final SoundTriggerListener mInternalCallback; 195 private final Callback mExternalCallback; 196 private final Object mLock = new Object(); 197 private final Handler mHandler; 198 199 private int mAvailability = STATE_NOT_READY; 200 201 /** 202 * Additional payload for {@link Callback#onDetected}. 203 */ 204 public static class EventPayload { 205 private final boolean mTriggerAvailable; 206 // Indicates if {@code captureSession} can be used to continue capturing more audio 207 // from the DSP hardware. 208 private final boolean mCaptureAvailable; 209 // The session to use when attempting to capture more audio from the DSP hardware. 210 private final int mCaptureSession; 211 private final AudioFormat mAudioFormat; 212 // Raw data associated with the event. 213 // This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true. 214 private final byte[] mData; 215 216 private EventPayload(boolean triggerAvailable, boolean captureAvailable, 217 AudioFormat audioFormat, int captureSession, byte[] data) { 218 mTriggerAvailable = triggerAvailable; 219 mCaptureAvailable = captureAvailable; 220 mCaptureSession = captureSession; 221 mAudioFormat = audioFormat; 222 mData = data; 223 } 224 225 /** 226 * Gets the format of the audio obtained using {@link #getTriggerAudio()}. 227 * May be null if there's no audio present. 228 */ 229 @Nullable 230 public AudioFormat getCaptureAudioFormat() { 231 return mAudioFormat; 232 } 233 234 /** 235 * Gets the raw audio that triggered the keyphrase. 236 * This may be null if the trigger audio isn't available. 237 * If non-null, the format of the audio can be obtained by calling 238 * {@link #getCaptureAudioFormat()}. 239 * 240 * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO 241 */ 242 @Nullable 243 public byte[] getTriggerAudio() { 244 if (mTriggerAvailable) { 245 return mData; 246 } else { 247 return null; 248 } 249 } 250 251 /** 252 * Gets the session ID to start a capture from the DSP. 253 * This may be null if streaming capture isn't possible. 254 * If non-null, the format of the audio that can be captured can be 255 * obtained using {@link #getCaptureAudioFormat()}. 256 * 257 * TODO: Candidate for Public API when the API to start capture with a session ID 258 * is made public. 259 * 260 * TODO: Add this to {@link #getCaptureAudioFormat()}: 261 * "Gets the format of the audio obtained using {@link #getTriggerAudio()} 262 * or {@link #getCaptureSession()}. May be null if no audio can be obtained 263 * for either the trigger or a streaming session." 264 * 265 * TODO: Should this return a known invalid value instead? 266 * 267 * @hide 268 */ 269 @Nullable 270 public Integer getCaptureSession() { 271 if (mCaptureAvailable) { 272 return mCaptureSession; 273 } else { 274 return null; 275 } 276 } 277 } 278 279 /** 280 * Callbacks for always-on hotword detection. 281 */ 282 public static abstract class Callback { 283 /** 284 * Called when the hotword availability changes. 285 * This indicates a change in the availability of recognition for the given keyphrase. 286 * It's called at least once with the initial availability.<p/> 287 * 288 * Availability implies whether the hardware on this system is capable of listening for 289 * the given keyphrase or not. <p/> 290 * 291 * @see AlwaysOnHotwordDetector#STATE_HARDWARE_UNAVAILABLE 292 * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNSUPPORTED 293 * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNENROLLED 294 * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_ENROLLED 295 */ 296 public abstract void onAvailabilityChanged(int status); 297 /** 298 * Called when the keyphrase is spoken. 299 * This implicitly stops listening for the keyphrase once it's detected. 300 * Clients should start a recognition again once they are done handling this 301 * detection. 302 * 303 * @param eventPayload Payload data for the detection event. 304 * This may contain the trigger audio, if requested when calling 305 * {@link AlwaysOnHotwordDetector#startRecognition(int)}. 306 */ 307 public abstract void onDetected(@NonNull EventPayload eventPayload); 308 /** 309 * Called when the detection fails due to an error. 310 */ 311 public abstract void onError(); 312 /** 313 * Called when the recognition is paused temporarily for some reason. 314 * This is an informational callback, and the clients shouldn't be doing anything here 315 * except showing an indication on their UI if they have to. 316 */ 317 public abstract void onRecognitionPaused(); 318 /** 319 * Called when the recognition is resumed after it was temporarily paused. 320 * This is an informational callback, and the clients shouldn't be doing anything here 321 * except showing an indication on their UI if they have to. 322 */ 323 public abstract void onRecognitionResumed(); 324 } 325 326 /** 327 * @param text The keyphrase text to get the detector for. 328 * @param locale The java locale for the detector. 329 * @param callback A non-null Callback for receiving the recognition events. 330 * @param voiceInteractionService The current voice interaction service. 331 * @param modelManagementService A service that allows management of sound models. 332 * 333 * @hide 334 */ 335 public AlwaysOnHotwordDetector(String text, Locale locale, Callback callback, 336 KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, 337 IVoiceInteractionService voiceInteractionService, 338 IVoiceInteractionManagerService modelManagementService) { 339 mText = text; 340 mLocale = locale; 341 mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo; 342 mKeyphraseMetadata = mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale); 343 mExternalCallback = callback; 344 mHandler = new MyHandler(); 345 mInternalCallback = new SoundTriggerListener(mHandler); 346 mVoiceInteractionService = voiceInteractionService; 347 mModelManagementService = modelManagementService; 348 new RefreshAvailabiltyTask().execute(); 349 } 350 351 /** 352 * Gets the recognition modes supported by the associated keyphrase. 353 * 354 * @see #RECOGNITION_MODE_USER_IDENTIFICATION 355 * @see #RECOGNITION_MODE_VOICE_TRIGGER 356 * 357 * @throws UnsupportedOperationException if the keyphrase itself isn't supported. 358 * Callers should only call this method after a supported state callback on 359 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 360 * @throws IllegalStateException if the detector is in an invalid state. 361 * This may happen if another detector has been instantiated or the 362 * {@link VoiceInteractionService} hosting this detector has been shut down. 363 */ 364 public @RecognitionModes int getSupportedRecognitionModes() { 365 if (DBG) Slog.d(TAG, "getSupportedRecognitionModes()"); 366 synchronized (mLock) { 367 return getSupportedRecognitionModesLocked(); 368 } 369 } 370 371 private int getSupportedRecognitionModesLocked() { 372 if (mAvailability == STATE_INVALID) { 373 throw new IllegalStateException( 374 "getSupportedRecognitionModes called on an invalid detector"); 375 } 376 377 // This method only makes sense if we can actually support a recognition. 378 if (mAvailability != STATE_KEYPHRASE_ENROLLED 379 && mAvailability != STATE_KEYPHRASE_UNENROLLED) { 380 throw new UnsupportedOperationException( 381 "Getting supported recognition modes for the keyphrase is not supported"); 382 } 383 384 return mKeyphraseMetadata.recognitionModeFlags; 385 } 386 387 /** 388 * Starts recognition for the associated keyphrase. 389 * 390 * @see #RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO 391 * @see #RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS 392 * 393 * @param recognitionFlags The flags to control the recognition properties. 394 * @return Indicates whether the call succeeded or not. 395 * @throws UnsupportedOperationException if the recognition isn't supported. 396 * Callers should only call this method after a supported state callback on 397 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 398 * @throws IllegalStateException if the detector is in an invalid state. 399 * This may happen if another detector has been instantiated or the 400 * {@link VoiceInteractionService} hosting this detector has been shut down. 401 */ 402 public boolean startRecognition(@RecognitionFlags int recognitionFlags) { 403 if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")"); 404 synchronized (mLock) { 405 if (mAvailability == STATE_INVALID) { 406 throw new IllegalStateException("startRecognition called on an invalid detector"); 407 } 408 409 // Check if we can start/stop a recognition. 410 if (mAvailability != STATE_KEYPHRASE_ENROLLED) { 411 throw new UnsupportedOperationException( 412 "Recognition for the given keyphrase is not supported"); 413 } 414 415 return startRecognitionLocked(recognitionFlags) == STATUS_OK; 416 } 417 } 418 419 /** 420 * Stops recognition for the associated keyphrase. 421 * 422 * @return Indicates whether the call succeeded or not. 423 * @throws UnsupportedOperationException if the recognition isn't supported. 424 * Callers should only call this method after a supported state callback on 425 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 426 * @throws IllegalStateException if the detector is in an invalid state. 427 * This may happen if another detector has been instantiated or the 428 * {@link VoiceInteractionService} hosting this detector has been shut down. 429 */ 430 public boolean stopRecognition() { 431 if (DBG) Slog.d(TAG, "stopRecognition()"); 432 synchronized (mLock) { 433 if (mAvailability == STATE_INVALID) { 434 throw new IllegalStateException("stopRecognition called on an invalid detector"); 435 } 436 437 // Check if we can start/stop a recognition. 438 if (mAvailability != STATE_KEYPHRASE_ENROLLED) { 439 throw new UnsupportedOperationException( 440 "Recognition for the given keyphrase is not supported"); 441 } 442 443 return stopRecognitionLocked() == STATUS_OK; 444 } 445 } 446 447 /** 448 * Creates an intent to start the enrollment for the associated keyphrase. 449 * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}. 450 * Starting re-enrollment is only valid if the keyphrase is un-enrolled, 451 * i.e. {@link #STATE_KEYPHRASE_UNENROLLED}, 452 * otherwise {@link #createReEnrollIntent()} should be preferred. 453 * 454 * @return An {@link Intent} to start enrollment for the given keyphrase. 455 * @throws UnsupportedOperationException if managing they keyphrase isn't supported. 456 * Callers should only call this method after a supported state callback on 457 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 458 * @throws IllegalStateException if the detector is in an invalid state. 459 * This may happen if another detector has been instantiated or the 460 * {@link VoiceInteractionService} hosting this detector has been shut down. 461 */ 462 public Intent createEnrollIntent() { 463 if (DBG) Slog.d(TAG, "createEnrollIntent"); 464 synchronized (mLock) { 465 return getManageIntentLocked(MANAGE_ACTION_ENROLL); 466 } 467 } 468 469 /** 470 * Creates an intent to start the un-enrollment for the associated keyphrase. 471 * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}. 472 * Starting re-enrollment is only valid if the keyphrase is already enrolled, 473 * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error. 474 * 475 * @return An {@link Intent} to start un-enrollment for the given keyphrase. 476 * @throws UnsupportedOperationException if managing they keyphrase isn't supported. 477 * Callers should only call this method after a supported state callback on 478 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 479 * @throws IllegalStateException if the detector is in an invalid state. 480 * This may happen if another detector has been instantiated or the 481 * {@link VoiceInteractionService} hosting this detector has been shut down. 482 */ 483 public Intent createUnEnrollIntent() { 484 if (DBG) Slog.d(TAG, "createUnEnrollIntent"); 485 synchronized (mLock) { 486 return getManageIntentLocked(MANAGE_ACTION_UN_ENROLL); 487 } 488 } 489 490 /** 491 * Creates an intent to start the re-enrollment for the associated keyphrase. 492 * This intent must be invoked using {@link Activity#startActivityForResult(Intent, int)}. 493 * Starting re-enrollment is only valid if the keyphrase is already enrolled, 494 * i.e. {@link #STATE_KEYPHRASE_ENROLLED}, otherwise invoking this may result in an error. 495 * 496 * @return An {@link Intent} to start re-enrollment for the given keyphrase. 497 * @throws UnsupportedOperationException if managing they keyphrase isn't supported. 498 * Callers should only call this method after a supported state callback on 499 * {@link Callback#onAvailabilityChanged(int)} to avoid this exception. 500 * @throws IllegalStateException if the detector is in an invalid state. 501 * This may happen if another detector has been instantiated or the 502 * {@link VoiceInteractionService} hosting this detector has been shut down. 503 */ 504 public Intent createReEnrollIntent() { 505 if (DBG) Slog.d(TAG, "createReEnrollIntent"); 506 synchronized (mLock) { 507 return getManageIntentLocked(MANAGE_ACTION_RE_ENROLL); 508 } 509 } 510 511 private Intent getManageIntentLocked(int action) { 512 if (mAvailability == STATE_INVALID) { 513 throw new IllegalStateException("getManageIntent called on an invalid detector"); 514 } 515 516 // This method only makes sense if we can actually support a recognition. 517 if (mAvailability != STATE_KEYPHRASE_ENROLLED 518 && mAvailability != STATE_KEYPHRASE_UNENROLLED) { 519 throw new UnsupportedOperationException( 520 "Managing the given keyphrase is not supported"); 521 } 522 523 return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale); 524 } 525 526 /** 527 * Invalidates this hotword detector so that any future calls to this result 528 * in an IllegalStateException. 529 * 530 * @hide 531 */ 532 void invalidate() { 533 synchronized (mLock) { 534 mAvailability = STATE_INVALID; 535 notifyStateChangedLocked(); 536 } 537 } 538 539 /** 540 * Reloads the sound models from the service. 541 * 542 * @hide 543 */ 544 void onSoundModelsChanged() { 545 synchronized (mLock) { 546 if (mAvailability == STATE_INVALID 547 || mAvailability == STATE_HARDWARE_UNAVAILABLE 548 || mAvailability == STATE_KEYPHRASE_UNSUPPORTED) { 549 Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config"); 550 return; 551 } 552 553 // Stop the recognition before proceeding. 554 // This is done because we want to stop the recognition on an older model if it changed 555 // or was deleted. 556 // The availability change callback should ensure that the client starts recognition 557 // again if needed. 558 stopRecognitionLocked(); 559 560 // Execute a refresh availability task - which should then notify of a change. 561 new RefreshAvailabiltyTask().execute(); 562 } 563 } 564 565 private int startRecognitionLocked(int recognitionFlags) { 566 KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1]; 567 // TODO: Do we need to do something about the confidence level here? 568 recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.id, 569 mKeyphraseMetadata.recognitionModeFlags, 0, new ConfidenceLevel[0]); 570 boolean captureTriggerAudio = 571 (recognitionFlags&RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0; 572 boolean allowMultipleTriggers = 573 (recognitionFlags&RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0; 574 int code = STATUS_ERROR; 575 try { 576 code = mModelManagementService.startRecognition(mVoiceInteractionService, 577 mKeyphraseMetadata.id, mLocale.toLanguageTag(), mInternalCallback, 578 new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers, 579 recognitionExtra, null /* additional data */)); 580 } catch (RemoteException e) { 581 Slog.w(TAG, "RemoteException in startRecognition!", e); 582 } 583 if (code != STATUS_OK) { 584 Slog.w(TAG, "startRecognition() failed with error code " + code); 585 } 586 return code; 587 } 588 589 private int stopRecognitionLocked() { 590 int code = STATUS_ERROR; 591 try { 592 code = mModelManagementService.stopRecognition( 593 mVoiceInteractionService, mKeyphraseMetadata.id, mInternalCallback); 594 } catch (RemoteException e) { 595 Slog.w(TAG, "RemoteException in stopRecognition!", e); 596 } 597 598 if (code != STATUS_OK) { 599 Slog.w(TAG, "stopRecognition() failed with error code " + code); 600 } 601 return code; 602 } 603 604 private void notifyStateChangedLocked() { 605 Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED); 606 message.arg1 = mAvailability; 607 message.sendToTarget(); 608 } 609 610 /** @hide */ 611 static final class SoundTriggerListener extends IRecognitionStatusCallback.Stub { 612 private final Handler mHandler; 613 614 public SoundTriggerListener(Handler handler) { 615 mHandler = handler; 616 } 617 618 @Override 619 public void onDetected(KeyphraseRecognitionEvent event) { 620 if (DBG) { 621 Slog.d(TAG, "onDetected(" + event + ")"); 622 } else { 623 Slog.i(TAG, "onDetected"); 624 } 625 Message.obtain(mHandler, MSG_HOTWORD_DETECTED, 626 new EventPayload(event.triggerInData, event.captureAvailable, 627 event.captureFormat, event.captureSession, event.data)) 628 .sendToTarget(); 629 } 630 631 @Override 632 public void onError(int status) { 633 Slog.i(TAG, "onError: " + status); 634 mHandler.sendEmptyMessage(MSG_DETECTION_ERROR); 635 } 636 637 @Override 638 public void onRecognitionPaused() { 639 Slog.i(TAG, "onRecognitionPaused"); 640 mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE); 641 } 642 643 @Override 644 public void onRecognitionResumed() { 645 Slog.i(TAG, "onRecognitionResumed"); 646 mHandler.sendEmptyMessage(MSG_DETECTION_RESUME); 647 } 648 } 649 650 class MyHandler extends Handler { 651 @Override 652 public void handleMessage(Message msg) { 653 synchronized (mLock) { 654 if (mAvailability == STATE_INVALID) { 655 Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector"); 656 return; 657 } 658 } 659 660 switch (msg.what) { 661 case MSG_AVAILABILITY_CHANGED: 662 mExternalCallback.onAvailabilityChanged(msg.arg1); 663 break; 664 case MSG_HOTWORD_DETECTED: 665 mExternalCallback.onDetected((EventPayload) msg.obj); 666 break; 667 case MSG_DETECTION_ERROR: 668 mExternalCallback.onError(); 669 break; 670 case MSG_DETECTION_PAUSE: 671 mExternalCallback.onRecognitionPaused(); 672 break; 673 case MSG_DETECTION_RESUME: 674 mExternalCallback.onRecognitionResumed(); 675 break; 676 default: 677 super.handleMessage(msg); 678 } 679 } 680 } 681 682 class RefreshAvailabiltyTask extends AsyncTask<Void, Void, Void> { 683 684 @Override 685 public Void doInBackground(Void... params) { 686 int availability = internalGetInitialAvailability(); 687 boolean enrolled = false; 688 // Fetch the sound model if the availability is one of the supported ones. 689 if (availability == STATE_NOT_READY 690 || availability == STATE_KEYPHRASE_UNENROLLED 691 || availability == STATE_KEYPHRASE_ENROLLED) { 692 enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id, mLocale); 693 if (!enrolled) { 694 availability = STATE_KEYPHRASE_UNENROLLED; 695 } else { 696 availability = STATE_KEYPHRASE_ENROLLED; 697 } 698 } 699 700 synchronized (mLock) { 701 if (DBG) { 702 Slog.d(TAG, "Hotword availability changed from " + mAvailability 703 + " -> " + availability); 704 } 705 mAvailability = availability; 706 notifyStateChangedLocked(); 707 } 708 return null; 709 } 710 711 /** 712 * @return The initial availability without checking the enrollment status. 713 */ 714 private int internalGetInitialAvailability() { 715 synchronized (mLock) { 716 // This detector has already been invalidated. 717 if (mAvailability == STATE_INVALID) { 718 return STATE_INVALID; 719 } 720 } 721 722 ModuleProperties dspModuleProperties = null; 723 try { 724 dspModuleProperties = 725 mModelManagementService.getDspModuleProperties(mVoiceInteractionService); 726 } catch (RemoteException e) { 727 Slog.w(TAG, "RemoteException in getDspProperties!", e); 728 } 729 // No DSP available 730 if (dspModuleProperties == null) { 731 return STATE_HARDWARE_UNAVAILABLE; 732 } 733 // No enrollment application supports this keyphrase/locale 734 if (mKeyphraseMetadata == null) { 735 return STATE_KEYPHRASE_UNSUPPORTED; 736 } 737 return STATE_NOT_READY; 738 } 739 740 /** 741 * @return The corresponding {@link KeyphraseSoundModel} or null if none is found. 742 */ 743 private boolean internalGetIsEnrolled(int keyphraseId, Locale locale) { 744 try { 745 return mModelManagementService.isEnrolledForKeyphrase( 746 mVoiceInteractionService, keyphraseId, locale.toLanguageTag()); 747 } catch (RemoteException e) { 748 Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!", e); 749 } 750 return false; 751 } 752 } 753 754 /** @hide */ 755 public void dump(String prefix, PrintWriter pw) { 756 synchronized (mLock) { 757 pw.print(prefix); pw.print("Text="); pw.println(mText); 758 pw.print(prefix); pw.print("Locale="); pw.println(mLocale); 759 pw.print(prefix); pw.print("Availability="); pw.println(mAvailability); 760 pw.print(prefix); pw.print("KeyphraseMetadata="); pw.println(mKeyphraseMetadata); 761 pw.print(prefix); pw.print("EnrollmentInfo="); pw.println(mKeyphraseEnrollmentInfo); 762 } 763 } 764 } 765