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.compat.annotation.UnsupportedAppUsage; 24 import android.content.Intent; 25 import android.hardware.soundtrigger.IRecognitionStatusCallback; 26 import android.hardware.soundtrigger.KeyphraseEnrollmentInfo; 27 import android.hardware.soundtrigger.KeyphraseMetadata; 28 import android.hardware.soundtrigger.SoundTrigger; 29 import android.hardware.soundtrigger.SoundTrigger.ConfidenceLevel; 30 import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent; 31 import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra; 32 import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; 33 import android.hardware.soundtrigger.SoundTrigger.ModuleProperties; 34 import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; 35 import android.media.AudioFormat; 36 import android.os.AsyncTask; 37 import android.os.Handler; 38 import android.os.Message; 39 import android.os.RemoteException; 40 import android.util.Slog; 41 42 import com.android.internal.app.IVoiceInteractionManagerService; 43 44 import java.io.PrintWriter; 45 import java.lang.annotation.Retention; 46 import java.lang.annotation.RetentionPolicy; 47 import java.util.Locale; 48 49 /** 50 * A class that lets a VoiceInteractionService implementation interact with 51 * always-on keyphrase detection APIs. 52 */ 53 public class AlwaysOnHotwordDetector { 54 //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----// 55 /** 56 * Indicates that this hotword detector is no longer valid for any recognition 57 * and should not be used anymore. 58 */ 59 private static final int STATE_INVALID = -3; 60 61 /** 62 * Indicates that recognition for the given keyphrase is not available on the system 63 * because of the hardware configuration. 64 * No further interaction should be performed with the detector that returns this availability. 65 */ 66 public static final int STATE_HARDWARE_UNAVAILABLE = -2; 67 /** 68 * Indicates that recognition for the given keyphrase is not supported. 69 * No further interaction should be performed with the detector that returns this availability. 70 */ 71 public static final int STATE_KEYPHRASE_UNSUPPORTED = -1; 72 /** 73 * Indicates that the given keyphrase is not enrolled. 74 * The caller may choose to begin an enrollment flow for the keyphrase. 75 */ 76 public static final int STATE_KEYPHRASE_UNENROLLED = 1; 77 /** 78 * Indicates that the given keyphrase is currently enrolled and it's possible to start 79 * recognition for it. 80 */ 81 public static final int STATE_KEYPHRASE_ENROLLED = 2; 82 83 /** 84 * Indicates that the detector isn't ready currently. 85 */ 86 private static final int STATE_NOT_READY = 0; 87 88 // Keyphrase management actions. Used in getManageIntent() ----// 89 @Retention(RetentionPolicy.SOURCE) 90 @IntDef(prefix = { "MANAGE_ACTION_" }, value = { 91 MANAGE_ACTION_ENROLL, 92 MANAGE_ACTION_RE_ENROLL, 93 MANAGE_ACTION_UN_ENROLL 94 }) 95 private @interface ManageActions {} 96 97 /** 98 * Indicates that we need to enroll. 99 * 100 * @hide 101 */ 102 public static final int MANAGE_ACTION_ENROLL = 0; 103 /** 104 * Indicates that we need to re-enroll. 105 * 106 * @hide 107 */ 108 public static final int MANAGE_ACTION_RE_ENROLL = 1; 109 /** 110 * Indicates that we need to un-enroll. 111 * 112 * @hide 113 */ 114 public static final int MANAGE_ACTION_UN_ENROLL = 2; 115 116 //-- Flags for startRecognition ----// 117 /** @hide */ 118 @Retention(RetentionPolicy.SOURCE) 119 @IntDef(flag = true, prefix = { "RECOGNITION_FLAG_" }, 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, prefix = { "RECOGNITION_MODE_" }, value = { 153 RECOGNITION_MODE_VOICE_TRIGGER, 154 RECOGNITION_MODE_USER_IDENTIFICATION, 155 }) 156 public @interface RecognitionModes {} 157 158 /** 159 * Simple recognition of the key phrase. 160 * Returned by {@link #getSupportedRecognitionModes()} 161 */ 162 public static final int RECOGNITION_MODE_VOICE_TRIGGER 163 = SoundTrigger.RECOGNITION_MODE_VOICE_TRIGGER; 164 /** 165 * User identification performed with the keyphrase recognition. 166 * Returned by {@link #getSupportedRecognitionModes()} 167 */ 168 public static final int RECOGNITION_MODE_USER_IDENTIFICATION 169 = SoundTrigger.RECOGNITION_MODE_USER_IDENTIFICATION; 170 171 static final String TAG = "AlwaysOnHotwordDetector"; 172 static final boolean DBG = false; 173 174 private static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR; 175 private static final int STATUS_OK = SoundTrigger.STATUS_OK; 176 177 private static final int MSG_AVAILABILITY_CHANGED = 1; 178 private static final int MSG_HOTWORD_DETECTED = 2; 179 private static final int MSG_DETECTION_ERROR = 3; 180 private static final int MSG_DETECTION_PAUSE = 4; 181 private static final int MSG_DETECTION_RESUME = 5; 182 183 private final String mText; 184 private final Locale mLocale; 185 /** 186 * The metadata of the Keyphrase, derived from the enrollment application. 187 * This may be null if this keyphrase isn't supported by the enrollment application. 188 */ 189 private final KeyphraseMetadata mKeyphraseMetadata; 190 private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; 191 private final IVoiceInteractionService mVoiceInteractionService; 192 private final IVoiceInteractionManagerService mModelManagementService; 193 private final SoundTriggerListener mInternalCallback; 194 private final Callback mExternalCallback; 195 private final Object mLock = new Object(); 196 private final Handler mHandler; 197 198 private int mAvailability = STATE_NOT_READY; 199 200 /** 201 * Additional payload for {@link Callback#onDetected}. 202 */ 203 public static class EventPayload { 204 private final boolean mTriggerAvailable; 205 // Indicates if {@code captureSession} can be used to continue capturing more audio 206 // from the DSP hardware. 207 private final boolean mCaptureAvailable; 208 // The session to use when attempting to capture more audio from the DSP hardware. 209 private final int mCaptureSession; 210 private final AudioFormat mAudioFormat; 211 // Raw data associated with the event. 212 // This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true. 213 private final byte[] mData; 214 EventPayload(boolean triggerAvailable, boolean captureAvailable, AudioFormat audioFormat, int captureSession, byte[] data)215 private EventPayload(boolean triggerAvailable, boolean captureAvailable, 216 AudioFormat audioFormat, int captureSession, byte[] data) { 217 mTriggerAvailable = triggerAvailable; 218 mCaptureAvailable = captureAvailable; 219 mCaptureSession = captureSession; 220 mAudioFormat = audioFormat; 221 mData = data; 222 } 223 224 /** 225 * Gets the format of the audio obtained using {@link #getTriggerAudio()}. 226 * May be null if there's no audio present. 227 */ 228 @Nullable getCaptureAudioFormat()229 public AudioFormat getCaptureAudioFormat() { 230 return mAudioFormat; 231 } 232 233 /** 234 * Gets the raw audio that triggered the keyphrase. 235 * This may be null if the trigger audio isn't available. 236 * If non-null, the format of the audio can be obtained by calling 237 * {@link #getCaptureAudioFormat()}. 238 * 239 * @see AlwaysOnHotwordDetector#RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO 240 */ 241 @Nullable getTriggerAudio()242 public byte[] getTriggerAudio() { 243 if (mTriggerAvailable) { 244 return mData; 245 } else { 246 return null; 247 } 248 } 249 250 /** 251 * Gets the session ID to start a capture from the DSP. 252 * This may be null if streaming capture isn't possible. 253 * If non-null, the format of the audio that can be captured can be 254 * obtained using {@link #getCaptureAudioFormat()}. 255 * 256 * TODO: Candidate for Public API when the API to start capture with a session ID 257 * is made public. 258 * 259 * TODO: Add this to {@link #getCaptureAudioFormat()}: 260 * "Gets the format of the audio obtained using {@link #getTriggerAudio()} 261 * or {@link #getCaptureSession()}. May be null if no audio can be obtained 262 * for either the trigger or a streaming session." 263 * 264 * TODO: Should this return a known invalid value instead? 265 * 266 * @hide 267 */ 268 @Nullable 269 @UnsupportedAppUsage getCaptureSession()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 */ onAvailabilityChanged(int status)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 */ onDetected(@onNull EventPayload eventPayload)307 public abstract void onDetected(@NonNull EventPayload eventPayload); 308 /** 309 * Called when the detection fails due to an error. 310 */ onError()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 */ onRecognitionPaused()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 */ onRecognitionResumed()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 */ AlwaysOnHotwordDetector(String text, Locale locale, Callback callback, KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, IVoiceInteractionService voiceInteractionService, IVoiceInteractionManagerService modelManagementService)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 */ getSupportedRecognitionModes()364 public @RecognitionModes int getSupportedRecognitionModes() { 365 if (DBG) Slog.d(TAG, "getSupportedRecognitionModes()"); 366 synchronized (mLock) { 367 return getSupportedRecognitionModesLocked(); 368 } 369 } 370 getSupportedRecognitionModesLocked()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 */ startRecognition(@ecognitionFlags int recognitionFlags)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 */ stopRecognition()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 */ createEnrollIntent()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 */ createUnEnrollIntent()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 */ createReEnrollIntent()504 public Intent createReEnrollIntent() { 505 if (DBG) Slog.d(TAG, "createReEnrollIntent"); 506 synchronized (mLock) { 507 return getManageIntentLocked(MANAGE_ACTION_RE_ENROLL); 508 } 509 } 510 getManageIntentLocked(int action)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 */ invalidate()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 */ onSoundModelsChanged()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 startRecognitionLocked(int recognitionFlags)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 stopRecognitionLocked()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 notifyStateChangedLocked()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 SoundTriggerListener(Handler handler)614 public SoundTriggerListener(Handler handler) { 615 mHandler = handler; 616 } 617 618 @Override onKeyphraseDetected(KeyphraseRecognitionEvent event)619 public void onKeyphraseDetected(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 @Override onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event)631 public void onGenericSoundTriggerDetected(SoundTrigger.GenericRecognitionEvent event) { 632 Slog.w(TAG, "Generic sound trigger event detected at AOHD: " + event); 633 } 634 635 @Override onError(int status)636 public void onError(int status) { 637 Slog.i(TAG, "onError: " + status); 638 mHandler.sendEmptyMessage(MSG_DETECTION_ERROR); 639 } 640 641 @Override onRecognitionPaused()642 public void onRecognitionPaused() { 643 Slog.i(TAG, "onRecognitionPaused"); 644 mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE); 645 } 646 647 @Override onRecognitionResumed()648 public void onRecognitionResumed() { 649 Slog.i(TAG, "onRecognitionResumed"); 650 mHandler.sendEmptyMessage(MSG_DETECTION_RESUME); 651 } 652 } 653 654 class MyHandler extends Handler { 655 @Override handleMessage(Message msg)656 public void handleMessage(Message msg) { 657 synchronized (mLock) { 658 if (mAvailability == STATE_INVALID) { 659 Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector"); 660 return; 661 } 662 } 663 664 switch (msg.what) { 665 case MSG_AVAILABILITY_CHANGED: 666 mExternalCallback.onAvailabilityChanged(msg.arg1); 667 break; 668 case MSG_HOTWORD_DETECTED: 669 mExternalCallback.onDetected((EventPayload) msg.obj); 670 break; 671 case MSG_DETECTION_ERROR: 672 mExternalCallback.onError(); 673 break; 674 case MSG_DETECTION_PAUSE: 675 mExternalCallback.onRecognitionPaused(); 676 break; 677 case MSG_DETECTION_RESUME: 678 mExternalCallback.onRecognitionResumed(); 679 break; 680 default: 681 super.handleMessage(msg); 682 } 683 } 684 } 685 686 class RefreshAvailabiltyTask extends AsyncTask<Void, Void, Void> { 687 688 @Override doInBackground(Void... params)689 public Void doInBackground(Void... params) { 690 int availability = internalGetInitialAvailability(); 691 boolean enrolled = false; 692 // Fetch the sound model if the availability is one of the supported ones. 693 if (availability == STATE_NOT_READY 694 || availability == STATE_KEYPHRASE_UNENROLLED 695 || availability == STATE_KEYPHRASE_ENROLLED) { 696 enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id, mLocale); 697 if (!enrolled) { 698 availability = STATE_KEYPHRASE_UNENROLLED; 699 } else { 700 availability = STATE_KEYPHRASE_ENROLLED; 701 } 702 } 703 704 synchronized (mLock) { 705 if (DBG) { 706 Slog.d(TAG, "Hotword availability changed from " + mAvailability 707 + " -> " + availability); 708 } 709 mAvailability = availability; 710 notifyStateChangedLocked(); 711 } 712 return null; 713 } 714 715 /** 716 * @return The initial availability without checking the enrollment status. 717 */ internalGetInitialAvailability()718 private int internalGetInitialAvailability() { 719 synchronized (mLock) { 720 // This detector has already been invalidated. 721 if (mAvailability == STATE_INVALID) { 722 return STATE_INVALID; 723 } 724 } 725 726 ModuleProperties dspModuleProperties = null; 727 try { 728 dspModuleProperties = 729 mModelManagementService.getDspModuleProperties(mVoiceInteractionService); 730 } catch (RemoteException e) { 731 Slog.w(TAG, "RemoteException in getDspProperties!", e); 732 } 733 // No DSP available 734 if (dspModuleProperties == null) { 735 return STATE_HARDWARE_UNAVAILABLE; 736 } 737 // No enrollment application supports this keyphrase/locale 738 if (mKeyphraseMetadata == null) { 739 return STATE_KEYPHRASE_UNSUPPORTED; 740 } 741 return STATE_NOT_READY; 742 } 743 744 /** 745 * @return The corresponding {@link KeyphraseSoundModel} or null if none is found. 746 */ internalGetIsEnrolled(int keyphraseId, Locale locale)747 private boolean internalGetIsEnrolled(int keyphraseId, Locale locale) { 748 try { 749 return mModelManagementService.isEnrolledForKeyphrase( 750 mVoiceInteractionService, keyphraseId, locale.toLanguageTag()); 751 } catch (RemoteException e) { 752 Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!", e); 753 } 754 return false; 755 } 756 } 757 758 /** @hide */ dump(String prefix, PrintWriter pw)759 public void dump(String prefix, PrintWriter pw) { 760 synchronized (mLock) { 761 pw.print(prefix); pw.print("Text="); pw.println(mText); 762 pw.print(prefix); pw.print("Locale="); pw.println(mLocale); 763 pw.print(prefix); pw.print("Availability="); pw.println(mAvailability); 764 pw.print(prefix); pw.print("KeyphraseMetadata="); pw.println(mKeyphraseMetadata); 765 pw.print(prefix); pw.print("EnrollmentInfo="); pw.println(mKeyphraseEnrollmentInfo); 766 } 767 } 768 } 769