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