1 /* 2 * Copyright (C) 2019 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 17 package com.android.server.soundtrigger_middleware; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.media.soundtrigger.ModelParameterRange; 22 import android.media.soundtrigger.PhraseSoundModel; 23 import android.media.soundtrigger.Properties; 24 import android.media.soundtrigger.RecognitionConfig; 25 import android.media.soundtrigger.SoundModel; 26 import android.media.soundtrigger.Status; 27 import android.media.soundtrigger_middleware.ISoundTriggerCallback; 28 import android.media.soundtrigger_middleware.ISoundTriggerModule; 29 import android.media.soundtrigger_middleware.PhraseRecognitionEventSys; 30 import android.media.soundtrigger_middleware.RecognitionEventSys; 31 import android.os.Binder; 32 import android.os.IBinder; 33 import android.os.RemoteException; 34 import android.util.Slog; 35 36 import java.util.ArrayList; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.Set; 43 44 /** 45 * This is an implementation of a single module of the ISoundTriggerMiddlewareService interface, 46 * exposing itself through the {@link ISoundTriggerModule} interface, possibly to multiple separate 47 * clients. 48 * <p> 49 * Typical usage is to query the module capabilities using {@link #getProperties()} and then to use 50 * the module through an {@link ISoundTriggerModule} instance, obtained via {@link 51 * #attach(ISoundTriggerCallback)}. Every such interface is its own session and state is not shared 52 * between sessions (i.e. cannot use a handle obtained from one session through another). 53 * <p> 54 * <b>Important conventions:</b> 55 * <ul> 56 * <li>Correct usage is assumed. This implementation does not attempt to gracefully handle 57 * invalid usage, and such usage will result in undefined behavior. If this service is to be 58 * offered to an untrusted client, it must be wrapped with input and state validation. 59 * <li>The underlying driver is assumed to be correct. This implementation does not attempt to 60 * gracefully handle driver malfunction and such behavior will result in undefined behavior. If this 61 * service is to used with an untrusted driver, the driver must be wrapped with validation / error 62 * recovery code. 63 * <li>Recovery from driver death is supported.</li> 64 * <li>RemoteExceptions thrown by the driver are treated as RuntimeExceptions - they are not 65 * considered recoverable faults and should not occur in a properly functioning system. 66 * <li>There is no binder instance associated with this implementation. Do not call asBinder(). 67 * <li>The implementation may throw a {@link RecoverableException} to indicate non-fatal, 68 * recoverable faults. The error code would one of the 69 * {@link android.media.soundtrigger.Status} constants. Any other exception thrown should be 70 * regarded as a bug in the implementation or one of its dependencies (assuming correct usage). 71 * <li>The implementation is designed for testability by featuring dependency injection (the 72 * underlying HAL driver instances are passed to the ctor) and by minimizing dependencies 73 * on Android runtime. 74 * <li>The implementation is thread-safe. This is achieved by a simplistic model, where all entry- 75 * points (both client API and driver callbacks) obtain a lock on the SoundTriggerModule instance 76 * for their entire scope. Any other method can be assumed to be running with the lock already 77 * obtained, so no further locking should be done. While this is not necessarily the most efficient 78 * synchronization strategy, it is very easy to reason about and this code is likely not on any 79 * performance-critical 80 * path. 81 * </ul> 82 * 83 * @hide 84 */ 85 class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.GlobalCallback { 86 static private final String TAG = "SoundTriggerModule"; 87 @NonNull private final HalFactory mHalFactory; 88 @NonNull private ISoundTriggerHal mHalService; 89 @NonNull private final SoundTriggerMiddlewareImpl.AudioSessionProvider mAudioSessionProvider; 90 private final Set<Session> mActiveSessions = new HashSet<>(); 91 private Properties mProperties; 92 93 /** 94 * Ctor. 95 * 96 * @param halFactory - A factory for the underlying HAL driver. 97 * @param audioSessionProvider - Creates a session token + device id/port pair used to 98 * associate recognition events with the audio stream used to access data. 99 */ SoundTriggerModule(@onNull HalFactory halFactory, @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider)100 SoundTriggerModule(@NonNull HalFactory halFactory, 101 @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider) { 102 mHalFactory = Objects.requireNonNull(halFactory); 103 mAudioSessionProvider = Objects.requireNonNull(audioSessionProvider); 104 105 attachToHal(); 106 } 107 108 /** 109 * Establish a client session with this module. 110 * 111 * This module may be shared by multiple clients, each will get its own session. While resources 112 * are shared between the clients, each session has its own state and data should not be shared 113 * across sessions. 114 * 115 * @param callback The client callback, which will be used for all messages. This is a oneway 116 * callback, so will never block, throw an unchecked exception or return a 117 * value. 118 * @return The interface through which this module can be controlled. 119 */ 120 synchronized @NonNull attach(@onNull ISoundTriggerCallback callback)121 ISoundTriggerModule attach(@NonNull ISoundTriggerCallback callback) { 122 Session session = new Session(callback); 123 mActiveSessions.add(session); 124 return session; 125 } 126 127 /** 128 * Query the module's properties. 129 * 130 * @return The properties structure. 131 */ 132 synchronized @NonNull getProperties()133 Properties getProperties() { 134 return mProperties; 135 } 136 137 @Override binderDied()138 public void binderDied() { 139 Slog.w(TAG, "Underlying HAL driver died."); 140 List<ISoundTriggerCallback> callbacks; 141 synchronized (this) { 142 callbacks = new ArrayList<>(mActiveSessions.size()); 143 for (Session session : mActiveSessions) { 144 callbacks.add(session.moduleDied()); 145 } 146 mActiveSessions.clear(); 147 reset(); 148 } 149 // Trigger the callbacks outside of the lock to avoid deadlocks. 150 for (ISoundTriggerCallback callback : callbacks) { 151 try { 152 callback.onModuleDied(); 153 } catch (RemoteException e) { 154 throw e.rethrowAsRuntimeException(); 155 } 156 } 157 } 158 159 /** 160 * Resets the transient state of this object. 161 */ reset()162 private void reset() { 163 mHalService.detach(); 164 attachToHal(); 165 } 166 167 /** 168 * Attached to the HAL service via factory. 169 */ attachToHal()170 private void attachToHal() { 171 mHalService = null; 172 while (mHalService == null) { 173 try { 174 mHalService = new SoundTriggerHalEnforcer( 175 new SoundTriggerHalWatchdog( 176 new SoundTriggerDuplicateModelHandler(mHalFactory.create()))); 177 } catch (RuntimeException e) { 178 if (!(e.getCause() instanceof RemoteException)) { 179 throw e; 180 } 181 } 182 } 183 mHalService.linkToDeath(this); 184 mHalService.registerCallback(this); 185 mProperties = mHalService.getProperties(); 186 } 187 188 /** 189 * Remove session from the list of active sessions. 190 * 191 * @param session The session to remove. 192 */ removeSession(@onNull Session session)193 private void removeSession(@NonNull Session session) { 194 mActiveSessions.remove(session); 195 } 196 197 @Override onResourcesAvailable()198 public void onResourcesAvailable() { 199 List<ISoundTriggerCallback> callbacks; 200 synchronized (this) { 201 callbacks = new ArrayList<>(mActiveSessions.size()); 202 for (Session session : mActiveSessions) { 203 callbacks.add(session.mCallback); 204 } 205 } 206 // Trigger the callbacks outside of the lock to avoid deadlocks. 207 for (ISoundTriggerCallback callback : callbacks) { 208 try { 209 callback.onResourcesAvailable(); 210 } catch (RemoteException e) { 211 throw e.rethrowAsRuntimeException(); 212 } 213 } 214 } 215 216 /** State of a single sound model. */ 217 private enum ModelState { 218 /** Initial state, until load() is called. */ 219 INIT, 220 /** Model is loaded, but recognition is not active. */ 221 LOADED, 222 /** Model is loaded and recognition is active. */ 223 ACTIVE 224 } 225 226 /** 227 * A single client session with this module. 228 * 229 * This is the main interface used to interact with this module. 230 */ 231 private class Session implements ISoundTriggerModule { 232 private ISoundTriggerCallback mCallback; 233 private final IBinder mToken = new Binder(); 234 private final Map<Integer, Model> mLoadedModels = new HashMap<>(); 235 236 /** 237 * Ctor. 238 * 239 * @param callback The client callback interface. 240 */ Session(@onNull ISoundTriggerCallback callback)241 private Session(@NonNull ISoundTriggerCallback callback) { 242 mCallback = callback; 243 mHalService.clientAttached(mToken); 244 } 245 246 @Override detach()247 public void detach() { 248 synchronized (SoundTriggerModule.this) { 249 if (mCallback == null) { 250 return; 251 } 252 removeSession(this); 253 mCallback = null; 254 mHalService.clientDetached(mToken); 255 } 256 } 257 258 @Override loadModel(@onNull SoundModel model)259 public int loadModel(@NonNull SoundModel model) { 260 synchronized (SoundTriggerModule.this) { 261 SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession = 262 mAudioSessionProvider.acquireSession(); 263 try { 264 checkValid(); 265 Model loadedModel = new Model(); 266 return loadedModel.load(model, audioSession); 267 } catch (Exception e) { 268 // We must do this outside the lock, to avoid possible deadlocks with the remote 269 // process that provides the audio sessions, which may also be calling into us. 270 try { 271 mAudioSessionProvider.releaseSession(audioSession.mSessionHandle); 272 } catch (Exception ee) { 273 Slog.e(TAG, "Failed to release session.", ee); 274 } 275 throw e; 276 } 277 } 278 } 279 280 @Override loadPhraseModel(@onNull PhraseSoundModel model)281 public int loadPhraseModel(@NonNull PhraseSoundModel model) { 282 synchronized (SoundTriggerModule.this) { 283 SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession = 284 mAudioSessionProvider.acquireSession(); 285 try { 286 checkValid(); 287 Model loadedModel = new Model(); 288 int result = loadedModel.load(model, audioSession); 289 Slog.d(TAG, String.format("loadPhraseModel()->%d", result)); 290 return result; 291 } catch (Exception e) { 292 // We must do this outside the lock, to avoid possible deadlocks with the remote 293 // process that provides the audio sessions, which may also be calling into us. 294 try { 295 mAudioSessionProvider.releaseSession(audioSession.mSessionHandle); 296 } catch (Exception ee) { 297 Slog.e(TAG, "Failed to release session.", ee); 298 } 299 throw e; 300 } 301 } 302 } 303 304 @Override unloadModel(int modelHandle)305 public void unloadModel(int modelHandle) { 306 synchronized (SoundTriggerModule.this) { 307 int sessionId; 308 checkValid(); 309 sessionId = mLoadedModels.get(modelHandle).unload(); 310 mAudioSessionProvider.releaseSession(sessionId); 311 } 312 } 313 314 @Override startRecognition(int modelHandle, @NonNull RecognitionConfig config)315 public IBinder startRecognition(int modelHandle, @NonNull RecognitionConfig config) { 316 synchronized (SoundTriggerModule.this) { 317 checkValid(); 318 return mLoadedModels.get(modelHandle).startRecognition(config); 319 } 320 } 321 322 @Override stopRecognition(int modelHandle)323 public void stopRecognition(int modelHandle) { 324 Model model; 325 synchronized (SoundTriggerModule.this) { 326 checkValid(); 327 model = mLoadedModels.get(modelHandle); 328 } 329 model.stopRecognition(); 330 } 331 332 @Override forceRecognitionEvent(int modelHandle)333 public void forceRecognitionEvent(int modelHandle) { 334 synchronized (SoundTriggerModule.this) { 335 checkValid(); 336 mLoadedModels.get(modelHandle).forceRecognitionEvent(); 337 } 338 } 339 340 @Override setModelParameter(int modelHandle, int modelParam, int value)341 public void setModelParameter(int modelHandle, int modelParam, int value) { 342 synchronized (SoundTriggerModule.this) { 343 checkValid(); 344 mLoadedModels.get(modelHandle).setParameter(modelParam, value); 345 } 346 } 347 348 @Override getModelParameter(int modelHandle, int modelParam)349 public int getModelParameter(int modelHandle, int modelParam) { 350 synchronized (SoundTriggerModule.this) { 351 checkValid(); 352 return mLoadedModels.get(modelHandle).getParameter(modelParam); 353 } 354 } 355 356 @Override 357 @Nullable queryModelParameterSupport(int modelHandle, int modelParam)358 public ModelParameterRange queryModelParameterSupport(int modelHandle, int modelParam) { 359 synchronized (SoundTriggerModule.this) { 360 checkValid(); 361 return mLoadedModels.get(modelHandle).queryModelParameterSupport(modelParam); 362 } 363 } 364 365 /** 366 * The underlying module HAL is dead. 367 * @return The client callback that needs to be invoked to notify the client. 368 */ moduleDied()369 private ISoundTriggerCallback moduleDied() { 370 ISoundTriggerCallback callback = mCallback; 371 mCallback = null; 372 return callback; 373 } 374 checkValid()375 private void checkValid() { 376 if (mCallback == null) { 377 throw new RecoverableException(Status.DEAD_OBJECT); 378 } 379 } 380 381 @Override 382 public @NonNull asBinder()383 IBinder asBinder() { 384 throw new UnsupportedOperationException( 385 "This implementation is not intended to be used directly with Binder."); 386 } 387 388 /** 389 * A single sound model in the system. 390 * 391 * All model-based operations are delegated to this class and implemented here. 392 */ 393 private class Model implements ISoundTriggerHal.ModelCallback { 394 public int mHandle; 395 private ModelState mState = ModelState.INIT; 396 private SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession mSession; 397 private IBinder mRecognitionToken = null; 398 private boolean mIsStopping = false; 399 400 private @NonNull getState()401 ModelState getState() { 402 return mState; 403 } 404 setState(@onNull ModelState state)405 private void setState(@NonNull ModelState state) { 406 mState = state; 407 SoundTriggerModule.this.notifyAll(); 408 } 409 load(@onNull SoundModel model, SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession)410 private int load(@NonNull SoundModel model, 411 SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession) { 412 mSession = audioSession; 413 mHandle = mHalService.loadSoundModel(model, this); 414 setState(ModelState.LOADED); 415 mLoadedModels.put(mHandle, this); 416 return mHandle; 417 } 418 load(@onNull PhraseSoundModel model, SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession)419 private int load(@NonNull PhraseSoundModel model, 420 SoundTriggerMiddlewareImpl.AudioSessionProvider.AudioSession audioSession) { 421 mSession = audioSession; 422 mHandle = mHalService.loadPhraseSoundModel(model, this); 423 424 setState(ModelState.LOADED); 425 mLoadedModels.put(mHandle, this); 426 return mHandle; 427 } 428 429 /** 430 * Unloads the model. 431 * @return The audio session handle. 432 */ unload()433 private int unload() { 434 mHalService.unloadSoundModel(mHandle); 435 mLoadedModels.remove(mHandle); 436 return mSession.mSessionHandle; 437 } 438 startRecognition(@onNull RecognitionConfig config)439 private IBinder startRecognition(@NonNull RecognitionConfig config) { 440 if (mIsStopping == true) { 441 throw new RecoverableException(Status.INTERNAL_ERROR, "Race occurred"); 442 } 443 mHalService.startRecognition(mHandle, mSession.mDeviceHandle, 444 mSession.mIoHandle, config); 445 mRecognitionToken = new Binder(); 446 setState(ModelState.ACTIVE); 447 return mRecognitionToken; 448 } 449 stopRecognition()450 private void stopRecognition() { 451 synchronized (SoundTriggerModule.this) { 452 if (getState() == ModelState.LOADED) { 453 // This call is idempotent in order to avoid races. 454 return; 455 } 456 mRecognitionToken = null; 457 mIsStopping = true; 458 } 459 mHalService.stopRecognition(mHandle); 460 synchronized (SoundTriggerModule.this) { 461 mIsStopping = false; 462 setState(ModelState.LOADED); 463 } 464 } 465 466 /** Request a forced recognition event. Will do nothing if recognition is inactive. */ forceRecognitionEvent()467 private void forceRecognitionEvent() { 468 if (getState() != ModelState.ACTIVE) { 469 // This call is idempotent in order to avoid races. 470 return; 471 } 472 mHalService.forceRecognitionEvent(mHandle); 473 } 474 475 setParameter(int modelParam, int value)476 private void setParameter(int modelParam, int value) { 477 mHalService.setModelParameter(mHandle, 478 ConversionUtil.aidl2hidlModelParameter(modelParam), value); 479 } 480 getParameter(int modelParam)481 private int getParameter(int modelParam) { 482 return mHalService.getModelParameter(mHandle, 483 ConversionUtil.aidl2hidlModelParameter(modelParam)); 484 } 485 486 @Nullable queryModelParameterSupport(int modelParam)487 private ModelParameterRange queryModelParameterSupport(int modelParam) { 488 return mHalService.queryParameter(mHandle, modelParam); 489 } 490 491 @Override recognitionCallback(int modelHandle, @NonNull RecognitionEventSys event)492 public void recognitionCallback(int modelHandle, 493 @NonNull RecognitionEventSys event) { 494 ISoundTriggerCallback callback; 495 synchronized (SoundTriggerModule.this) { 496 if (mRecognitionToken == null) { 497 return; 498 } 499 event.token = mRecognitionToken; 500 if (!event.recognitionEvent.recognitionStillActive) { 501 setState(ModelState.LOADED); 502 mRecognitionToken = null; 503 } 504 callback = mCallback; 505 } 506 // The callback must be invoked outside of the lock. 507 try { 508 if (callback != null) { 509 callback.onRecognition(mHandle, event, mSession.mSessionHandle); 510 } 511 } catch (RemoteException e) { 512 // We're not expecting any exceptions here. 513 throw e.rethrowAsRuntimeException(); 514 } 515 } 516 517 @Override phraseRecognitionCallback(int modelHandle, @NonNull PhraseRecognitionEventSys event)518 public void phraseRecognitionCallback(int modelHandle, 519 @NonNull PhraseRecognitionEventSys event) { 520 ISoundTriggerCallback callback; 521 synchronized (SoundTriggerModule.this) { 522 if (mRecognitionToken == null) { 523 return; 524 } 525 event.token = mRecognitionToken; 526 if (!event.phraseRecognitionEvent.common.recognitionStillActive) { 527 setState(ModelState.LOADED); 528 mRecognitionToken = null; 529 } 530 callback = mCallback; 531 } 532 // The callback must be invoked outside of the lock. 533 try { 534 if (callback != null) { 535 mCallback.onPhraseRecognition(mHandle, event, mSession.mSessionHandle); 536 } 537 } catch (RemoteException e) { 538 // We're not expecting any exceptions here. 539 throw e.rethrowAsRuntimeException(); 540 } 541 } 542 543 @Override modelUnloaded(int modelHandle)544 public void modelUnloaded(int modelHandle) { 545 ISoundTriggerCallback callback; 546 synchronized (SoundTriggerModule.this) { 547 callback = mCallback; 548 } 549 550 // The callback must be invoked outside of the lock. 551 try { 552 if (callback != null) { 553 callback.onModelUnloaded(modelHandle); 554 } 555 } catch (RemoteException e) { 556 // We're not expecting any exceptions here. 557 throw e.rethrowAsRuntimeException(); 558 } 559 } 560 } 561 } 562 } 563