SoundTriggerHelper.java revision 5e33fb057c20b84418d96574abe861e9d05956eb
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 com.android.server.voiceinteraction; 18 19import android.hardware.soundtrigger.IRecognitionStatusCallback; 20import android.hardware.soundtrigger.SoundTrigger; 21import android.hardware.soundtrigger.SoundTrigger.Keyphrase; 22import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionEvent; 23import android.hardware.soundtrigger.SoundTrigger.KeyphraseRecognitionExtra; 24import android.hardware.soundtrigger.SoundTrigger.KeyphraseSoundModel; 25import android.hardware.soundtrigger.SoundTrigger.ModuleProperties; 26import android.hardware.soundtrigger.SoundTrigger.RecognitionConfig; 27import android.hardware.soundtrigger.SoundTrigger.RecognitionEvent; 28import android.hardware.soundtrigger.SoundTriggerModule; 29import android.os.RemoteException; 30import android.util.Slog; 31import android.util.SparseArray; 32 33import java.util.ArrayList; 34import java.util.UUID; 35 36/** 37 * Helper for {@link SoundTrigger} APIs. 38 * Currently this just acts as an abstraction over all SoundTrigger API calls. 39 * 40 * @hide 41 */ 42public class SoundTriggerHelper implements SoundTrigger.StatusListener { 43 static final String TAG = "SoundTriggerHelper"; 44 // TODO: Set to false. 45 static final boolean DBG = true; 46 47 /** 48 * Return codes for {@link #startRecognition(int, KeyphraseSoundModel, 49 * IRecognitionStatusCallback, RecognitionConfig)}, 50 * {@link #stopRecognition(int, IRecognitionStatusCallback)} 51 */ 52 public static final int STATUS_ERROR = SoundTrigger.STATUS_ERROR; 53 public static final int STATUS_OK = SoundTrigger.STATUS_OK; 54 55 private static final int INVALID_SOUND_MODEL_HANDLE = -1; 56 57 /** The {@link DspInfo} for the system, or null if none exists. */ 58 final ModuleProperties moduleProperties; 59 60 /** The properties for the DSP module */ 61 private final SoundTriggerModule mModule; 62 63 // Use a RemoteCallbackList here? 64 private final SparseArray<IRecognitionStatusCallback> mActiveListeners; 65 66 private int mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; 67 private UUID mCurrentSoundModelUuid = null; 68 // FIXME: Ideally this should not be stored if allowMultipleTriggers happens at a lower layer. 69 private RecognitionConfig mRecognitionConfig = null; 70 71 SoundTriggerHelper() { 72 ArrayList <ModuleProperties> modules = new ArrayList<>(); 73 int status = SoundTrigger.listModules(modules); 74 mActiveListeners = new SparseArray<>(1); 75 if (status != SoundTrigger.STATUS_OK || modules.size() == 0) { 76 Slog.w(TAG, "listModules status=" + status + ", # of modules=" + modules.size()); 77 moduleProperties = null; 78 mModule = null; 79 } else { 80 // TODO: Figure out how to determine which module corresponds to the DSP hardware. 81 moduleProperties = modules.get(0); 82 mModule = SoundTrigger.attachModule(moduleProperties.id, this, null); 83 } 84 } 85 86 /** 87 * @return True, if a recognition for the given {@link Keyphrase} is active. 88 */ 89 synchronized boolean isKeyphraseActive(Keyphrase keyphrase) { 90 if (keyphrase == null) { 91 Slog.w(TAG, "isKeyphraseActive requires a non-null keyphrase"); 92 return false; 93 } 94 return mActiveListeners.get(keyphrase.id) != null; 95 } 96 97 /** 98 * Starts recognition for the given keyphraseId. 99 * 100 * @param keyphraseId The identifier of the keyphrase for which 101 * the recognition is to be started. 102 * @param soundModel The sound model to use for recognition. 103 * @param listener The listener for the recognition events related to the given keyphrase. 104 * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}. 105 */ 106 synchronized int startRecognition(int keyphraseId, 107 KeyphraseSoundModel soundModel, 108 IRecognitionStatusCallback listener, 109 RecognitionConfig recognitionConfig) { 110 if (DBG) { 111 Slog.d(TAG, "startRecognition for keyphraseId=" + keyphraseId 112 + " soundModel=" + soundModel + ", listener=" + listener 113 + ", recognitionConfig=" + recognitionConfig); 114 Slog.d(TAG, "moduleProperties=" + moduleProperties); 115 Slog.d(TAG, "# of current listeners=" + mActiveListeners.size()); 116 Slog.d(TAG, "current SoundModel handle=" + mCurrentSoundModelHandle); 117 Slog.d(TAG, "current SoundModel UUID=" 118 + (mCurrentSoundModelUuid == null ? null : mCurrentSoundModelUuid)); 119 } 120 if (moduleProperties == null || mModule == null) { 121 Slog.w(TAG, "Attempting startRecognition without the capability"); 122 return STATUS_ERROR; 123 } 124 125 if (mCurrentSoundModelHandle != INVALID_SOUND_MODEL_HANDLE 126 && !soundModel.uuid.equals(mCurrentSoundModelUuid)) { 127 Slog.w(TAG, "Unloading previous sound model"); 128 int status = mModule.unloadSoundModel(mCurrentSoundModelHandle); 129 if (status != SoundTrigger.STATUS_OK) { 130 Slog.w(TAG, "unloadSoundModel call failed with " + status); 131 return status; 132 } 133 mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; 134 mCurrentSoundModelUuid = null; 135 } 136 137 // If the previous recognition was by a different listener, 138 // Notify them that it was stopped. 139 IRecognitionStatusCallback oldListener = mActiveListeners.get(keyphraseId); 140 if (oldListener != null && oldListener.asBinder() != listener.asBinder()) { 141 Slog.w(TAG, "Canceling previous recognition"); 142 try { 143 oldListener.onError(STATUS_ERROR); 144 } catch (RemoteException e) { 145 Slog.w(TAG, "RemoteException in onDetectionStopped"); 146 } 147 mActiveListeners.remove(keyphraseId); 148 } 149 150 // Load the sound model if the current one is null. 151 int soundModelHandle = mCurrentSoundModelHandle; 152 if (mCurrentSoundModelHandle == INVALID_SOUND_MODEL_HANDLE 153 || mCurrentSoundModelUuid == null) { 154 int[] handle = new int[] { INVALID_SOUND_MODEL_HANDLE }; 155 int status = mModule.loadSoundModel(soundModel, handle); 156 if (status != SoundTrigger.STATUS_OK) { 157 Slog.w(TAG, "loadSoundModel call failed with " + status); 158 return status; 159 } 160 if (handle[0] == INVALID_SOUND_MODEL_HANDLE) { 161 Slog.w(TAG, "loadSoundModel call returned invalid sound model handle"); 162 return STATUS_ERROR; 163 } 164 soundModelHandle = handle[0]; 165 } else { 166 if (DBG) Slog.d(TAG, "Reusing previously loaded sound model"); 167 } 168 169 // Start the recognition. 170 int status = mModule.startRecognition(soundModelHandle, recognitionConfig); 171 if (status != SoundTrigger.STATUS_OK) { 172 Slog.w(TAG, "startRecognition failed with " + status); 173 return status; 174 } 175 176 // Everything went well! 177 mCurrentSoundModelHandle = soundModelHandle; 178 mCurrentSoundModelUuid = soundModel.uuid; 179 mRecognitionConfig = recognitionConfig; 180 // Register the new listener. This replaces the old one. 181 // There can only be a maximum of one active listener for a keyphrase 182 // at any given time. 183 mActiveListeners.put(keyphraseId, listener); 184 return STATUS_OK; 185 } 186 187 /** 188 * Stops recognition for the given {@link Keyphrase} if a recognition is 189 * currently active. 190 * 191 * @param keyphraseId The identifier of the keyphrase for which 192 * the recognition is to be stopped. 193 * @param listener The listener for the recognition events related to the given keyphrase. 194 * 195 * @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}. 196 */ 197 synchronized int stopRecognition(int keyphraseId, IRecognitionStatusCallback listener) { 198 if (DBG) { 199 Slog.d(TAG, "stopRecognition for keyphraseId=" + keyphraseId 200 + ", listener=" + listener); 201 Slog.d(TAG, "# of current listeners = " + mActiveListeners.size()); 202 } 203 204 if (moduleProperties == null || mModule == null) { 205 Slog.w(TAG, "Attempting stopRecognition without the capability"); 206 return STATUS_ERROR; 207 } 208 209 IRecognitionStatusCallback currentListener = mActiveListeners.get(keyphraseId); 210 if (listener == null) { 211 Slog.w(TAG, "Attempting stopRecognition without a valid listener"); 212 return STATUS_ERROR; 213 } if (currentListener == null) { 214 // startRecognition hasn't been called or it failed. 215 Slog.w(TAG, "Attempting stopRecognition without a successful startRecognition"); 216 return STATUS_ERROR; 217 } else if (currentListener.asBinder() != listener.asBinder()) { 218 // We don't allow a different listener to stop the recognition than the one 219 // that started it. 220 Slog.w(TAG, "Attempting stopRecognition for another recognition"); 221 return STATUS_ERROR; 222 } else { 223 // Stop recognition if it's the current one, ignore otherwise. 224 int status = mModule.stopRecognition(mCurrentSoundModelHandle); 225 if (status != SoundTrigger.STATUS_OK) { 226 Slog.w(TAG, "stopRecognition call failed with " + status); 227 return status; 228 } 229 status = mModule.unloadSoundModel(mCurrentSoundModelHandle); 230 if (status != SoundTrigger.STATUS_OK) { 231 Slog.w(TAG, "unloadSoundModel call failed with " + status); 232 return status; 233 } 234 235 mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; 236 mCurrentSoundModelUuid = null; 237 238 mActiveListeners.remove(keyphraseId); 239 return STATUS_OK; 240 } 241 } 242 243 synchronized void stopAllRecognitions() { 244 if (moduleProperties == null || mModule == null) { 245 return; 246 } 247 248 if (mCurrentSoundModelHandle == INVALID_SOUND_MODEL_HANDLE) { 249 return; 250 } 251 252 int status = mModule.stopRecognition(mCurrentSoundModelHandle); 253 if (status != SoundTrigger.STATUS_OK) { 254 Slog.w(TAG, "stopRecognition call failed with " + status); 255 } 256 status = mModule.unloadSoundModel(mCurrentSoundModelHandle); 257 if (status != SoundTrigger.STATUS_OK) { 258 Slog.w(TAG, "unloadSoundModel call failed with " + status); 259 } 260 261 mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; 262 mCurrentSoundModelUuid = null; 263 264 mActiveListeners.clear(); 265 } 266 267 //---- SoundTrigger.StatusListener methods 268 @Override 269 public void onRecognition(RecognitionEvent event) { 270 if (event == null) { 271 Slog.w(TAG, "Invalid recognition event!"); 272 return; 273 } 274 275 if (DBG) Slog.d(TAG, "onRecognition: " + event); 276 switch (event.status) { 277 // Fire aborts/failures to all listeners since it's not tied to a keyphrase. 278 case SoundTrigger.RECOGNITION_STATUS_ABORT: // fall-through 279 case SoundTrigger.RECOGNITION_STATUS_FAILURE: 280 try { 281 synchronized (this) { 282 for (int i = 0; i < mActiveListeners.size(); i++) { 283 mActiveListeners.valueAt(i).onError(STATUS_ERROR); 284 } 285 } 286 } catch (RemoteException e) { 287 Slog.w(TAG, "RemoteException in onDetectionStopped"); 288 } 289 break; 290 case SoundTrigger.RECOGNITION_STATUS_SUCCESS: 291 if (!(event instanceof KeyphraseRecognitionEvent)) { 292 Slog.w(TAG, "Invalid recognition event!"); 293 return; 294 } 295 296 KeyphraseRecognitionExtra[] keyphraseExtras = 297 ((KeyphraseRecognitionEvent) event).keyphraseExtras; 298 if (keyphraseExtras == null || keyphraseExtras.length == 0) { 299 Slog.w(TAG, "Invalid keyphrase recognition event!"); 300 return; 301 } 302 // TODO: Handle more than one keyphrase extras. 303 int keyphraseId = keyphraseExtras[0].id; 304 try { 305 synchronized(this) { 306 // Check which keyphrase triggered, and fire the appropriate event. 307 IRecognitionStatusCallback listener = mActiveListeners.get(keyphraseId); 308 if (listener != null) { 309 listener.onDetected((KeyphraseRecognitionEvent) event); 310 } else { 311 Slog.w(TAG, "received onRecognition event without any listener for it"); 312 return; 313 } 314 315 // FIXME: Remove this block if the lower layer supports multiple triggers. 316 if (mRecognitionConfig != null 317 && mRecognitionConfig.allowMultipleTriggers) { 318 int status = mModule.startRecognition( 319 mCurrentSoundModelHandle, mRecognitionConfig); 320 if (status != STATUS_OK) { 321 Slog.w(TAG, "Error in restarting recognition after a trigger"); 322 listener.onError(status); 323 } 324 } 325 } 326 } catch (RemoteException e) { 327 Slog.w(TAG, "RemoteException in onDetectionStopped"); 328 } 329 break; 330 } 331 } 332 333 @Override 334 public void onServiceDied() { 335 synchronized (this) { 336 try { 337 for (int i = 0; i < mActiveListeners.size(); i++) { 338 mActiveListeners.valueAt(i).onError(SoundTrigger.STATUS_DEAD_OBJECT); 339 } 340 } catch (RemoteException e) { 341 Slog.w(TAG, "RemoteException in onDetectionStopped"); 342 } 343 mCurrentSoundModelHandle = INVALID_SOUND_MODEL_HANDLE; 344 mCurrentSoundModelUuid = null; 345 // Remove all listeners. 346 mActiveListeners.clear(); 347 } 348 } 349} 350