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