AlwaysOnHotwordDetector.java revision d7018200312e4e4dc3f67cf33dc90bf7ce585844
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.content.Intent;
20import android.hardware.soundtrigger.Keyphrase;
21import android.hardware.soundtrigger.KeyphraseEnrollmentInfo;
22import android.hardware.soundtrigger.KeyphraseMetadata;
23import android.hardware.soundtrigger.SoundTriggerHelper;
24import android.util.Slog;
25
26/**
27 * A class that lets a VoiceInteractionService implementation interact with
28 * always-on keyphrase detection APIs.
29 */
30public class AlwaysOnHotwordDetector {
31    //---- States of Keyphrase availability ----//
32    /**
33     * Indicates that the given keyphrase is not available on the system because of the
34     * hardware configuration.
35     */
36    public static final int KEYPHRASE_HARDWARE_UNAVAILABLE = -2;
37    /**
38     * Indicates that the given keyphrase is not supported.
39     */
40    public static final int KEYPHRASE_UNSUPPORTED = -1;
41    /**
42     * Indicates that the given keyphrase is not enrolled.
43     */
44    public static final int KEYPHRASE_UNENROLLED = 1;
45    /**
46     * Indicates that the given keyphrase is currently enrolled but not being actively listened for.
47     */
48    public static final int KEYPHRASE_ENROLLED = 2;
49
50    // Keyphrase management actions ----//
51    /** Indicates that we need to enroll. */
52    public static final int MANAGE_ACTION_ENROLL = 0;
53    /** Indicates that we need to re-enroll. */
54    public static final int MANAGE_ACTION_RE_ENROLL = 1;
55    /** Indicates that we need to un-enroll. */
56    public static final int MANAGE_ACTION_UN_ENROLL = 2;
57
58    /**
59     * Return codes for {@link #startRecognition()}, {@link #stopRecognition()}
60     */
61    public static final int STATUS_ERROR = Integer.MIN_VALUE;
62    public static final int STATUS_OK = 1;
63
64    //---- Keyphrase recognition status ----//
65    // TODO: Figure out if they are exclusive or should be flags instead?
66    public static final int RECOGNITION_NOT_AVAILABLE = -3;
67    public static final int RECOGNITION_NOT_REQUESTED = -2;
68    public static final int RECOGNITION_DISABLED_TEMPORARILY = -1;
69    public static final int RECOGNITION_REQUESTED = 1;
70    public static final int RECOGNITION_ACTIVE = 2;
71    static final String TAG = "AlwaysOnHotwordDetector";
72
73    private final String mText;
74    private final String mLocale;
75    private final Keyphrase mKeyphrase;
76    private final KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo;
77    private final SoundTriggerHelper mSoundTriggerHelper;
78    private final SoundTriggerHelper.Listener mListener;
79    private final int mAvailability;
80
81    private int mRecognitionState;
82
83    /**
84     * Callbacks for always-on hotword detection.
85     */
86    public interface Callback {
87        /**
88         * Called when the keyphrase is spoken.
89         * TODO: Add more data to the callback.
90         */
91        void onDetected();
92        /**
93         * Called when the detection for the associated keyphrase starts.
94         */
95        void onDetectionStarted();
96        /**
97         * Called when the detection for the associated keyphrase stops.
98         */
99        void onDetectionStopped();
100    }
101
102    /**
103     * @param text The keyphrase text to get the detector for.
104     * @param locale The java locale for the detector.
105     * @param callback A non-null Callback for receiving the recognition events.
106     *
107     * @hide
108     */
109    public AlwaysOnHotwordDetector(String text, String locale, Callback callback,
110            KeyphraseEnrollmentInfo keyphraseEnrollmentInfo,
111            SoundTriggerHelper soundTriggerHelper) {
112        mText = text;
113        mLocale = locale;
114        mKeyphraseEnrollmentInfo = keyphraseEnrollmentInfo;
115        KeyphraseMetadata keyphraseMetadata =
116                mKeyphraseEnrollmentInfo.getKeyphraseMetadata(text, locale);
117        if (keyphraseMetadata != null) {
118            mKeyphrase = new Keyphrase(keyphraseMetadata.id, text, locale);
119        } else {
120            mKeyphrase = null;
121        }
122        mListener = new SoundTriggerListener(callback);
123        mSoundTriggerHelper = soundTriggerHelper;
124        mAvailability = getAvailabilityInternal();
125    }
126
127    /**
128     * Gets the state of always-on hotword detection for the given keyphrase and locale
129     * on this system.
130     * Availability implies that the hardware on this system is capable of listening for
131     * the given keyphrase or not.
132     *
133     * @return Indicates if always-on hotword detection is available for the given keyphrase.
134     *         The return code is one of {@link #KEYPHRASE_HARDWARE_UNAVAILABLE},
135     *         {@link #KEYPHRASE_UNSUPPORTED}, {@link #KEYPHRASE_UNENROLLED} or
136     *         {@link #KEYPHRASE_ENROLLED}.
137     */
138    public int getAvailability() {
139        return mAvailability;
140    }
141
142    /**
143     * Gets the status of the recognition.
144     * @return One of {@link #RECOGNITION_NOT_AVAILABLE}, {@link #RECOGNITION_NOT_REQUESTED},
145     *         {@link #RECOGNITION_DISABLED_TEMPORARILY} or {@link #RECOGNITION_ACTIVE}.
146     * @throws UnsupportedOperationException if the recognition isn't supported.
147     *         Callers should check the availability by calling {@link #getAvailability()}
148     *         before calling this method to avoid this exception.
149     */
150    public int getRecognitionStatus() {
151        if (mAvailability != KEYPHRASE_ENROLLED) {
152            throw new UnsupportedOperationException(
153                    "Recognition for the given keyphrase is not supported");
154        }
155
156        return mRecognitionState;
157    }
158
159    /**
160     * Starts recognition for the associated keyphrase.
161     *
162     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
163     * @throws UnsupportedOperationException if the recognition isn't supported.
164     *         Callers should check the availability by calling {@link #getAvailability()}
165     *         before calling this method to avoid this exception.
166     */
167    public int startRecognition() {
168        if (mAvailability != KEYPHRASE_ENROLLED) {
169            throw new UnsupportedOperationException(
170                    "Recognition for the given keyphrase is not supported");
171        }
172
173        mRecognitionState = RECOGNITION_REQUESTED;
174        int code = mSoundTriggerHelper.startRecognition(mKeyphrase.id, mListener);
175        if (code != SoundTriggerHelper.STATUS_OK) {
176            Slog.w(TAG, "startRecognition() failed with error code " + code);
177            return STATUS_ERROR;
178        } else {
179            return STATUS_OK;
180        }
181    }
182
183    /**
184     * Stops recognition for the associated keyphrase.
185     *
186     * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
187     * @throws UnsupportedOperationException if the recognition isn't supported.
188     *         Callers should check the availability by calling {@link #getAvailability()}
189     *         before calling this method to avoid this exception.
190     */
191    public int stopRecognition() {
192        if (mAvailability != KEYPHRASE_ENROLLED) {
193            throw new UnsupportedOperationException(
194                    "Recognition for the given keyphrase is not supported");
195        }
196
197        mRecognitionState = RECOGNITION_NOT_REQUESTED;
198        int code = mSoundTriggerHelper.stopRecognition(mKeyphrase.id, mListener);
199        if (code != SoundTriggerHelper.STATUS_OK) {
200            Slog.w(TAG, "stopRecognition() failed with error code " + code);
201            return STATUS_ERROR;
202        } else {
203            return STATUS_OK;
204        }
205    }
206
207    /**
208     * Gets an intent to manage the associated keyphrase.
209     *
210     * @param action The manage action that needs to be performed.
211     *        One of {@link #MANAGE_ACTION_ENROLL}, {@link #MANAGE_ACTION_RE_ENROLL} or
212     *        {@link #MANAGE_ACTION_UN_ENROLL}.
213     * @return An {@link Intent} to manage the given keyphrase.
214     * @throws UnsupportedOperationException if managing they keyphrase isn't supported.
215     *         Callers should check the availability by calling {@link #getAvailability()}
216     *         before calling this method to avoid this exception.
217     */
218    public Intent getManageIntent(int action) {
219        if (mAvailability == KEYPHRASE_HARDWARE_UNAVAILABLE
220                || mAvailability == KEYPHRASE_UNSUPPORTED) {
221            throw new UnsupportedOperationException(
222                    "Managing the given keyphrase is not supported");
223        }
224        if (action != MANAGE_ACTION_ENROLL
225                && action != MANAGE_ACTION_RE_ENROLL
226                && action != MANAGE_ACTION_UN_ENROLL) {
227            throw new IllegalArgumentException("Invalid action specified " + action);
228        }
229
230        return mKeyphraseEnrollmentInfo.getManageKeyphraseIntent(action, mText, mLocale);
231    }
232
233    private int getAvailabilityInternal() {
234        if (mSoundTriggerHelper.dspInfo == null) {
235            return KEYPHRASE_HARDWARE_UNAVAILABLE;
236        }
237        if (mKeyphrase == null || !mSoundTriggerHelper.isKeyphraseSupported(mKeyphrase)) {
238            return KEYPHRASE_UNSUPPORTED;
239        }
240        if (!mSoundTriggerHelper.isKeyphraseEnrolled(mKeyphrase)) {
241            return KEYPHRASE_UNENROLLED;
242        }
243        return KEYPHRASE_ENROLLED;
244    }
245
246    /** @hide */
247    static final class SoundTriggerListener implements SoundTriggerHelper.Listener {
248        private final Callback mCallback;
249
250        public SoundTriggerListener(Callback callback) {
251            this.mCallback = callback;
252        }
253
254        @Override
255        public void onKeyphraseSpoken() {
256            Slog.i(TAG, "onKeyphraseSpoken");
257            mCallback.onDetected();
258        }
259
260        @Override
261        public void onListeningStateChanged(int state) {
262            Slog.i(TAG, "onListeningStateChanged: state=" + state);
263            if (state == SoundTriggerHelper.STATE_STARTED) {
264                mCallback.onDetectionStarted();
265            } else if (state == SoundTriggerHelper.STATE_STOPPED) {
266                mCallback.onDetectionStopped();
267            }
268        }
269    }
270}
271