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