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