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