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