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