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