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