AlwaysOnHotwordDetector.java revision fd7070fdff6864abf750beaef697cc2fdb8c5eb9
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        /**
189         * Indicates if {@code data} is the audio that triggered the keyphrase.
190         */
191        public final boolean triggerAvailable;
192        /**
193         * Indicates if {@code captureSession} can be used to continue capturing more audio from
194         * the DSP hardware.
195         *
196         * Candidate for public API
197         * @hide
198         */
199        public final boolean captureAvailable;
200        /**
201         * The session to use when attempting to capture more audio from the DSP hardware.
202         *
203         * Candidate for public API
204         * TODO: When unhiding, change javadoc of audioFormat to -
205         * "Format of {@code data} or the audio that may be captured using {@code captureSession}.
206         * May be null if {@code triggerAvailable} and {@code captureAvailable} are false."
207         * @hide
208         */
209        public final int captureSession;
210        /**
211         * Format of {@code data}.
212         * May be null if {@code triggerAvailable} is false.
213         */
214        @Nullable
215        public final AudioFormat audioFormat;
216        /**
217         * Raw data associated with the event.
218         * This is the audio that triggered the keyphrase if {@code isTriggerAudio} is true.
219         */
220        @Nullable
221        public final byte[] data;
222
223        private EventPayload(boolean _triggerAvailable, boolean _captureAvailable,
224                AudioFormat _audioFormat, int _captureSession, byte[] _data) {
225            triggerAvailable = _triggerAvailable;
226            captureAvailable = _captureAvailable;
227            captureSession = _captureSession;
228            audioFormat = _audioFormat;
229            data = _data;
230        }
231    }
232
233    /**
234     * Callbacks for always-on hotword detection.
235     */
236    public interface Callback {
237        /**
238         * Called when the hotword availability changes.
239         * This indicates a change in the availability of recognition for the given keyphrase.
240         * It's called at least once with the initial availability.<p/>
241         *
242         * Availability implies whether the hardware on this system is capable of listening for
243         * the given keyphrase or not. <p/>
244         *
245         * @see AlwaysOnHotwordDetector#STATE_HARDWARE_UNAVAILABLE
246         * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNSUPPORTED
247         * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_UNENROLLED
248         * @see AlwaysOnHotwordDetector#STATE_KEYPHRASE_ENROLLED
249         */
250        void onAvailabilityChanged(int status);
251        /**
252         * Called when the keyphrase is spoken.
253         * This implicitly stops listening for the keyphrase once it's detected.
254         * Clients should start a recognition again once they are done handling this
255         * detection.
256         *
257         * @param eventPayload Payload data for the detection event.
258         *        This may contain the trigger audio, if requested when calling
259         *        {@link AlwaysOnHotwordDetector#startRecognition(int)}.
260         */
261        void onDetected(@NonNull EventPayload eventPayload);
262        /**
263         * Called when the detection fails due to an error.
264         */
265        void onError();
266        /**
267         * Called when the recognition is paused temporarily for some reason.
268         * This is an informational callback, and the clients shouldn't be doing anything here
269         * except showing an indication on their UI if they have to.
270         */
271        void onRecognitionPaused();
272        /**
273         * Called when the recognition is resumed after it was temporarily paused.
274         * This is an informational callback, and the clients shouldn't be doing anything here
275         * except showing an indication on their UI if they have to.
276         */
277        void onRecognitionResumed();
278    }
279
280    /**
281     * @param text The keyphrase text to get the detector for.
282     * @param locale The java locale for the detector.
283     * @param callback A non-null Callback for receiving the recognition events.
284     * @param voiceInteractionService The current voice interaction service.
285     * @param modelManagementService A service that allows management of sound models.
286     *
287     * @hide
288     */
289    public AlwaysOnHotwordDetector(String text, String locale, Callback callback,
290            KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
291            IVoiceInteractionService voiceInteractionService,
292            IVoiceInteractionManagerService modelManagementService) {
293        mText = text;
294        mLocale = locale;
295        mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
296        mKeyphraseMetadata = mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale);
297        mExternalCallback = callback;
298        mHandler = new MyHandler();
299        mInternalCallback = new SoundTriggerListener(mHandler);
300        mVoiceInteractionService = voiceInteractionService;
301        mModelManagementService = modelManagementService;
302        new RefreshAvailabiltyTask().execute();
303    }
304
305    /**
306     * Gets the recognition modes supported by the associated keyphrase.
307     *
308     * @see #RECOGNITION_MODE_USER_IDENTIFICATION
309     * @see #RECOGNITION_MODE_VOICE_TRIGGER
310     *
311     * @throws UnsupportedOperationException if the keyphrase itself isn't supported.
312     *         Callers should only call this method after a supported state callback on
313     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
314     * @throws IllegalStateException if the detector is in an invalid state.
315     *         This may happen if another detector has been instantiated or the
316     *         {@link VoiceInteractionService} hosting this detector has been shut down.
317     */
318    public @RecognitionModes int getSupportedRecognitionModes() {
319        if (DBG) Slog.d(TAG, "getSupportedRecognitionModes()");
320        synchronized (mLock) {
321            return getSupportedRecognitionModesLocked();
322        }
323    }
324
325    private int getSupportedRecognitionModesLocked() {
326        if (mAvailability == STATE_INVALID) {
327            throw new IllegalStateException(
328                    "getSupportedRecognitionModes called on an invalid detector");
329        }
330
331        // This method only makes sense if we can actually support a recognition.
332        if (mAvailability != STATE_KEYPHRASE_ENROLLED
333                && mAvailability != STATE_KEYPHRASE_UNENROLLED) {
334            throw new UnsupportedOperationException(
335                    "Getting supported recognition modes for the keyphrase is not supported");
336        }
337
338        return mKeyphraseMetadata.recognitionModeFlags;
339    }
340
341    /**
342     * Starts recognition for the associated keyphrase.
343     *
344     * @param recognitionFlags The flags to control the recognition properties.
345     *        The allowed flags are {@link #RECOGNITION_FLAG_NONE},
346     *        {@link #RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO} and
347     *        {@link #RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS}.
348     * @return Indicates whether the call succeeded or not.
349     * @throws UnsupportedOperationException if the recognition isn't supported.
350     *         Callers should only call this method after a supported state callback on
351     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
352     * @throws IllegalStateException if the detector is in an invalid state.
353     *         This may happen if another detector has been instantiated or the
354     *         {@link VoiceInteractionService} hosting this detector has been shut down.
355     */
356    public boolean startRecognition(@RecognitionFlags int recognitionFlags) {
357        if (DBG) Slog.d(TAG, "startRecognition(" + recognitionFlags + ")");
358        synchronized (mLock) {
359            if (mAvailability == STATE_INVALID) {
360                throw new IllegalStateException("startRecognition called on an invalid detector");
361            }
362
363            // Check if we can start/stop a recognition.
364            if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
365                throw new UnsupportedOperationException(
366                        "Recognition for the given keyphrase is not supported");
367            }
368
369            return startRecognitionLocked(recognitionFlags) == STATUS_OK;
370        }
371    }
372
373    /**
374     * Stops recognition for the associated keyphrase.
375     *
376     * @return Indicates whether the call succeeded or not.
377     * @throws UnsupportedOperationException if the recognition isn't supported.
378     *         Callers should only call this method after a supported state callback on
379     *         {@link Callback#onAvailabilityChanged(int)} to avoid this exception.
380     * @throws IllegalStateException if the detector is in an invalid state.
381     *         This may happen if another detector has been instantiated or the
382     *         {@link VoiceInteractionService} hosting this detector has been shut down.
383     */
384    public boolean stopRecognition() {
385        if (DBG) Slog.d(TAG, "stopRecognition()");
386        synchronized (mLock) {
387            if (mAvailability == STATE_INVALID) {
388                throw new IllegalStateException("stopRecognition called on an invalid detector");
389            }
390
391            // Check if we can start/stop a recognition.
392            if (mAvailability != STATE_KEYPHRASE_ENROLLED) {
393                throw new UnsupportedOperationException(
394                        "Recognition for the given keyphrase is not supported");
395            }
396
397            return stopRecognitionLocked() == STATUS_OK;
398        }
399    }
400
401    /**
402     * Gets an intent to manage the associated keyphrase.
403     *
404     * @param action The manage action that needs to be performed.
405     *        One of {@link #MANAGE_ACTION_ENROLL}, {@link #MANAGE_ACTION_RE_ENROLL} or
406     *        {@link #MANAGE_ACTION_UN_ENROLL}.
407     * @return An {@link Intent} to manage the given keyphrase.
408     * @throws UnsupportedOperationException if managing they keyphrase 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 Intent getManageIntent(@ManageActions int action) {
416        if (DBG) Slog.d(TAG, "getManageIntent(" + action + ")");
417        synchronized (mLock) {
418            return getManageIntentLocked(action);
419        }
420    }
421
422    private Intent getManageIntentLocked(int action) {
423        if (mAvailability == STATE_INVALID) {
424            throw new IllegalStateException("getManageIntent called on an invalid detector");
425        }
426
427        // This method only makes sense if we can actually support a recognition.
428        if (mAvailability != STATE_KEYPHRASE_ENROLLED
429                && mAvailability != STATE_KEYPHRASE_UNENROLLED) {
430            throw new UnsupportedOperationException(
431                    "Managing the given keyphrase is not supported");
432        }
433
434        if (action != MANAGE_ACTION_ENROLL
435                && action != MANAGE_ACTION_RE_ENROLL
436                && action != MANAGE_ACTION_UN_ENROLL) {
437            throw new IllegalArgumentException("Invalid action specified " + action);
438        }
439
440        return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale);
441    }
442
443    /**
444     * Invalidates this hotword detector so that any future calls to this result
445     * in an IllegalStateException.
446     *
447     * @hide
448     */
449    void invalidate() {
450        synchronized (mLock) {
451            mAvailability = STATE_INVALID;
452            notifyStateChangedLocked();
453        }
454    }
455
456    /**
457     * Reloads the sound models from the service.
458     *
459     * @hide
460     */
461    void onSoundModelsChanged() {
462        synchronized (mLock) {
463            // FIXME: This should stop the recognition if it was using an enrolled sound model
464            // that's no longer available.
465            if (mAvailability == STATE_INVALID
466                    || mAvailability == STATE_HARDWARE_UNAVAILABLE
467                    || mAvailability == STATE_KEYPHRASE_UNSUPPORTED) {
468                Slog.w(TAG, "Received onSoundModelsChanged for an unsupported keyphrase/config");
469                return;
470            }
471
472            // Execute a refresh availability task - which should then notify of a change.
473            new RefreshAvailabiltyTask().execute();
474        }
475    }
476
477    private int startRecognitionLocked(int recognitionFlags) {
478        KeyphraseRecognitionExtra[] recognitionExtra = new KeyphraseRecognitionExtra[1];
479        // TODO: Do we need to do something about the confidence level here?
480        recognitionExtra[0] = new KeyphraseRecognitionExtra(mKeyphraseMetadata.id,
481                mKeyphraseMetadata.recognitionModeFlags, 0, new ConfidenceLevel[0]);
482        boolean captureTriggerAudio =
483                (recognitionFlags&RECOGNITION_FLAG_CAPTURE_TRIGGER_AUDIO) != 0;
484        boolean allowMultipleTriggers =
485                (recognitionFlags&RECOGNITION_FLAG_ALLOW_MULTIPLE_TRIGGERS) != 0;
486        int code = STATUS_ERROR;
487        try {
488            code = mModelManagementService.startRecognition(mVoiceInteractionService,
489                    mKeyphraseMetadata.id, mInternalCallback,
490                    new RecognitionConfig(captureTriggerAudio, allowMultipleTriggers,
491                            recognitionExtra, null /* additional data */));
492        } catch (RemoteException e) {
493            Slog.w(TAG, "RemoteException in startRecognition!");
494        }
495        if (code != STATUS_OK) {
496            Slog.w(TAG, "startRecognition() failed with error code " + code);
497        }
498        return code;
499    }
500
501    private int stopRecognitionLocked() {
502        int code = STATUS_ERROR;
503        try {
504            code = mModelManagementService.stopRecognition(
505                    mVoiceInteractionService, mKeyphraseMetadata.id, mInternalCallback);
506        } catch (RemoteException e) {
507            Slog.w(TAG, "RemoteException in stopRecognition!");
508        }
509
510        if (code != STATUS_OK) {
511            Slog.w(TAG, "stopRecognition() failed with error code " + code);
512        }
513        return code;
514    }
515
516    private void notifyStateChangedLocked() {
517        Message message = Message.obtain(mHandler, MSG_AVAILABILITY_CHANGED);
518        message.arg1 = mAvailability;
519        message.sendToTarget();
520    }
521
522    /** @hide */
523    static final class SoundTriggerListener extends IRecognitionStatusCallback.Stub {
524        private final Handler mHandler;
525
526        public SoundTriggerListener(Handler handler) {
527            mHandler = handler;
528        }
529
530        @Override
531        public void onDetected(KeyphraseRecognitionEvent event) {
532            if (DBG) {
533                Slog.d(TAG, "onDetected(" + event + ")");
534            } else {
535                Slog.i(TAG, "onDetected");
536            }
537            Message.obtain(mHandler, MSG_HOTWORD_DETECTED,
538                    new EventPayload(event.triggerInData, event.captureAvailable,
539                            event.captureFormat, event.captureSession, event.data))
540                    .sendToTarget();
541        }
542
543        @Override
544        public void onError(int status) {
545            Slog.i(TAG, "onError: " + status);
546            mHandler.sendEmptyMessage(MSG_DETECTION_ERROR);
547        }
548
549        @Override
550        public void onRecognitionPaused() {
551            Slog.i(TAG, "onRecognitionPaused");
552            mHandler.sendEmptyMessage(MSG_DETECTION_PAUSE);
553        }
554
555        @Override
556        public void onRecognitionResumed() {
557            Slog.i(TAG, "onRecognitionResumed");
558            mHandler.sendEmptyMessage(MSG_DETECTION_RESUME);
559        }
560    }
561
562    class MyHandler extends Handler {
563        @Override
564        public void handleMessage(Message msg) {
565            synchronized (mLock) {
566                if (mAvailability == STATE_INVALID) {
567                    Slog.w(TAG, "Received message: " + msg.what + " for an invalid detector");
568                    return;
569                }
570            }
571
572            switch (msg.what) {
573                case MSG_AVAILABILITY_CHANGED:
574                    mExternalCallback.onAvailabilityChanged(msg.arg1);
575                    break;
576                case MSG_HOTWORD_DETECTED:
577                    mExternalCallback.onDetected((EventPayload) msg.obj);
578                    break;
579                case MSG_DETECTION_ERROR:
580                    mExternalCallback.onError();
581                    break;
582                case MSG_DETECTION_PAUSE:
583                    mExternalCallback.onRecognitionPaused();
584                    break;
585                case MSG_DETECTION_RESUME:
586                    mExternalCallback.onRecognitionResumed();
587                    break;
588                default:
589                    super.handleMessage(msg);
590            }
591        }
592    }
593
594    class RefreshAvailabiltyTask extends AsyncTask<Void, Void, Void> {
595
596        @Override
597        public Void doInBackground(Void... params) {
598            int availability = internalGetInitialAvailability();
599            boolean enrolled = false;
600            // Fetch the sound model if the availability is one of the supported ones.
601            if (availability == STATE_NOT_READY
602                    || availability == STATE_KEYPHRASE_UNENROLLED
603                    || availability == STATE_KEYPHRASE_ENROLLED) {
604                enrolled = internalGetIsEnrolled(mKeyphraseMetadata.id);
605                if (!enrolled) {
606                    availability = STATE_KEYPHRASE_UNENROLLED;
607                } else {
608                    availability = STATE_KEYPHRASE_ENROLLED;
609                }
610            }
611
612            synchronized (mLock) {
613                if (DBG) {
614                    Slog.d(TAG, "Hotword availability changed from " + mAvailability
615                            + " -> " + availability);
616                }
617                mAvailability = availability;
618                notifyStateChangedLocked();
619            }
620            return null;
621        }
622
623        /**
624         * @return The initial availability without checking the enrollment status.
625         */
626        private int internalGetInitialAvailability() {
627            synchronized (mLock) {
628                // This detector has already been invalidated.
629                if (mAvailability == STATE_INVALID) {
630                    return STATE_INVALID;
631                }
632            }
633
634            ModuleProperties dspModuleProperties = null;
635            try {
636                dspModuleProperties =
637                        mModelManagementService.getDspModuleProperties(mVoiceInteractionService);
638            } catch (RemoteException e) {
639                Slog.w(TAG, "RemoteException in getDspProperties!");
640            }
641            // No DSP available
642            if (dspModuleProperties == null) {
643                return STATE_HARDWARE_UNAVAILABLE;
644            }
645            // No enrollment application supports this keyphrase/locale
646            if (mKeyphraseMetadata == null) {
647                return STATE_KEYPHRASE_UNSUPPORTED;
648            }
649            return STATE_NOT_READY;
650        }
651
652        /**
653         * @return The corresponding {@link KeyphraseSoundModel} or null if none is found.
654         */
655        private boolean internalGetIsEnrolled(int keyphraseId) {
656            try {
657                return mModelManagementService.isEnrolledForKeyphrase(
658                        mVoiceInteractionService, keyphraseId);
659            } catch (RemoteException e) {
660                Slog.w(TAG, "RemoteException in listRegisteredKeyphraseSoundModels!");
661            }
662            return false;
663        }
664    }
665}
666