1 /* 2 * Copyright (C) 2023 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.Nullable; 20 import android.hardware.soundtrigger3.ISoundTriggerHw; 21 import android.hardware.soundtrigger3.ISoundTriggerHwCallback; 22 import android.hardware.soundtrigger3.ISoundTriggerHwGlobalCallback; 23 import android.media.soundtrigger.ModelParameter; 24 import android.media.soundtrigger.ModelParameterRange; 25 import android.media.soundtrigger.PhraseRecognitionEvent; 26 import android.media.soundtrigger.PhraseRecognitionExtra; 27 import android.media.soundtrigger.PhraseSoundModel; 28 import android.media.soundtrigger.Properties; 29 import android.media.soundtrigger.RecognitionConfig; 30 import android.media.soundtrigger.RecognitionEvent; 31 import android.media.soundtrigger.RecognitionMode; 32 import android.media.soundtrigger.RecognitionStatus; 33 import android.media.soundtrigger.SoundModel; 34 import android.media.soundtrigger.SoundModelType; 35 import android.media.soundtrigger.Status; 36 import android.media.soundtrigger_middleware.IAcknowledgeEvent; 37 import android.media.soundtrigger_middleware.IInjectGlobalEvent; 38 import android.media.soundtrigger_middleware.IInjectModelEvent; 39 import android.media.soundtrigger_middleware.IInjectRecognitionEvent; 40 import android.media.soundtrigger_middleware.ISoundTriggerInjection; 41 import android.os.DeadObjectException; 42 import android.os.IBinder; 43 import android.os.Parcel; 44 import android.os.RemoteException; 45 import android.os.ServiceSpecificException; 46 import android.util.Slog; 47 48 import com.android.internal.annotations.GuardedBy; 49 import com.android.internal.util.FunctionalUtils; 50 51 import java.util.HashMap; 52 import java.util.Map; 53 import java.util.NoSuchElementException; 54 import java.util.concurrent.Executor; 55 import java.util.concurrent.Executors; 56 57 58 /** 59 * Fake HAL implementation, which offers injection via 60 * {@link ISoundTriggerInjection}. 61 * Since this is a test interface, upon unexpected operations from the framework, 62 * we will abort. 63 */ 64 public class FakeSoundTriggerHal extends ISoundTriggerHw.Stub { 65 private static final String TAG = "FakeSoundTriggerHal"; 66 67 // Fake values for valid model param range 68 private static final int THRESHOLD_MIN = -10; 69 private static final int THRESHOLD_MAX = 10; 70 71 // Logically const 72 private final Object mLock = new Object(); 73 private final Properties mProperties; 74 75 // These cannot be injected, since we rely on: 76 // 1) Serialization 77 // 2) Running in a different thread 78 // And there is no Executor interface with these requirements 79 // These factories clean up the pools on finalizer. 80 // Package private so the FakeHalFactory can dispatch 81 static class ExecutorHolder { 82 static final Executor CALLBACK_EXECUTOR = 83 Executors.newSingleThreadExecutor(); 84 static final Executor INJECTION_EXECUTOR = 85 Executors.newSingleThreadExecutor(); 86 } 87 88 // Dispatcher interface for callbacks, using the executors above 89 private final InjectionDispatcher mInjectionDispatcher; 90 91 // Created on construction, passed back to clients. 92 private final IInjectGlobalEvent.Stub mGlobalEventSession; 93 94 @GuardedBy("mLock") 95 private IBinder.DeathRecipient mDeathRecipient; 96 97 @GuardedBy("mLock") 98 private GlobalCallbackDispatcher mGlobalCallbackDispatcher = null; 99 100 @GuardedBy("mLock") 101 private boolean mIsResourceContended = false; 102 @GuardedBy("mLock") 103 private final Map<Integer, ModelSession> mModelSessionMap = new HashMap<>(); 104 105 // Current version of the STHAL relies on integer model session ids. 106 // Generate them monotonically starting at 101 107 @GuardedBy("mLock") 108 private int mModelKeyCounter = 101; 109 110 @GuardedBy("mLock") 111 private boolean mIsDead = false; 112 113 private class ModelSession extends IInjectModelEvent.Stub { 114 // Logically const 115 private final boolean mIsKeyphrase; 116 private final CallbackDispatcher mCallbackDispatcher; 117 private final int mModelHandle; 118 119 // Model parameter 120 @GuardedBy("FakeSoundTriggerHal.this.mLock") 121 private int mThreshold = 0; 122 123 // Mutable 124 @GuardedBy("FakeSoundTriggerHal.this.mLock") 125 private boolean mIsUnloaded = false; // Latch 126 127 // Only a single recognition session is able to be active for a model 128 // session at any given time. Null if no recognition is active. 129 @GuardedBy("FakeSoundTriggerHal.this.mLock") 130 @Nullable private RecognitionSession mRecognitionSession; 131 ModelSession(int modelHandle, CallbackDispatcher callbackDispatcher, boolean isKeyphrase)132 private ModelSession(int modelHandle, CallbackDispatcher callbackDispatcher, 133 boolean isKeyphrase) { 134 mModelHandle = modelHandle; 135 mCallbackDispatcher = callbackDispatcher; 136 mIsKeyphrase = isKeyphrase; 137 } 138 startRecognitionForModel()139 private RecognitionSession startRecognitionForModel() { 140 synchronized (FakeSoundTriggerHal.this.mLock) { 141 mRecognitionSession = new RecognitionSession(); 142 return mRecognitionSession; 143 } 144 } 145 stopRecognitionForModel()146 private RecognitionSession stopRecognitionForModel() { 147 synchronized (FakeSoundTriggerHal.this.mLock) { 148 RecognitionSession session = mRecognitionSession; 149 mRecognitionSession = null; 150 return session; 151 } 152 } 153 forceRecognitionForModel()154 private void forceRecognitionForModel() { 155 synchronized (FakeSoundTriggerHal.this.mLock) { 156 if (mIsKeyphrase) { 157 PhraseRecognitionEvent phraseEvent = 158 createDefaultKeyphraseEvent(RecognitionStatus.FORCED); 159 mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> 160 cb.phraseRecognitionCallback(mModelHandle, phraseEvent)); 161 } else { 162 RecognitionEvent event = createDefaultEvent(RecognitionStatus.FORCED); 163 mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> 164 cb.recognitionCallback(mModelHandle, event)); 165 } 166 } 167 } 168 setThresholdFactor(int value)169 private void setThresholdFactor(int value) { 170 synchronized (FakeSoundTriggerHal.this.mLock) { 171 mThreshold = value; 172 } 173 } 174 getThresholdFactor()175 private int getThresholdFactor() { 176 synchronized (FakeSoundTriggerHal.this.mLock) { 177 return mThreshold; 178 } 179 } 180 getIsUnloaded()181 private boolean getIsUnloaded() { 182 synchronized (FakeSoundTriggerHal.this.mLock) { 183 return mIsUnloaded; 184 } 185 } 186 getRecogSession()187 private RecognitionSession getRecogSession() { 188 synchronized (FakeSoundTriggerHal.this.mLock) { 189 return mRecognitionSession; 190 } 191 } 192 193 194 /** oneway **/ 195 @Override triggerUnloadModel()196 public void triggerUnloadModel() { 197 synchronized (FakeSoundTriggerHal.this.mLock) { 198 if (mIsDead || mIsUnloaded) return; 199 if (mRecognitionSession != null) { 200 // Must abort model before triggering unload 201 mRecognitionSession.triggerAbortRecognition(); 202 } 203 // Invalidate the model session 204 mIsUnloaded = true; 205 mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> 206 cb.modelUnloaded(mModelHandle)); 207 // Don't notify the injection that an unload has occurred, since it is what 208 // triggered the unload 209 210 // Notify if we could have denied a previous model due to contention 211 if (getNumLoadedModelsLocked() == (mProperties.maxSoundModels - 1) 212 && !mIsResourceContended) { 213 mGlobalCallbackDispatcher.wrap((ISoundTriggerHwGlobalCallback cb) -> 214 cb.onResourcesAvailable()); 215 } 216 } 217 } 218 219 private class RecognitionSession extends IInjectRecognitionEvent.Stub { 220 221 @Override 222 /** oneway **/ triggerRecognitionEvent(byte[] data, @Nullable PhraseRecognitionExtra[] phraseExtras)223 public void triggerRecognitionEvent(byte[] data, 224 @Nullable PhraseRecognitionExtra[] phraseExtras) { 225 synchronized (FakeSoundTriggerHal.this.mLock) { 226 // Check if our session has already been invalidated 227 if (mIsDead || mRecognitionSession != this) return; 228 // Invalidate the recognition session 229 mRecognitionSession = null; 230 // Trigger the callback. 231 if (mIsKeyphrase) { 232 PhraseRecognitionEvent phraseEvent = 233 createDefaultKeyphraseEvent(RecognitionStatus.SUCCESS); 234 phraseEvent.common.data = data; 235 if (phraseExtras != null) phraseEvent.phraseExtras = phraseExtras; 236 mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> 237 cb.phraseRecognitionCallback(mModelHandle, phraseEvent)); 238 } else { 239 RecognitionEvent event = createDefaultEvent(RecognitionStatus.SUCCESS); 240 event.data = data; 241 mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> 242 cb.recognitionCallback(mModelHandle, event)); 243 } 244 } 245 } 246 247 @Override 248 /** oneway **/ triggerAbortRecognition()249 public void triggerAbortRecognition() { 250 synchronized (FakeSoundTriggerHal.this.mLock) { 251 if (mIsDead || mRecognitionSession != this) return; 252 // Clear the session state 253 mRecognitionSession = null; 254 // Trigger the callback. 255 if (mIsKeyphrase) { 256 mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> 257 cb.phraseRecognitionCallback(mModelHandle, 258 createDefaultKeyphraseEvent(RecognitionStatus.ABORTED))); 259 } else { 260 mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> 261 cb.recognitionCallback(mModelHandle, 262 createDefaultEvent(RecognitionStatus.ABORTED))); 263 } 264 } 265 } 266 } 267 } 268 269 // Since this is always constructed, it needs to be cheap to create. FakeSoundTriggerHal(ISoundTriggerInjection injection)270 public FakeSoundTriggerHal(ISoundTriggerInjection injection) { 271 mProperties = createDefaultProperties(); 272 mInjectionDispatcher = new InjectionDispatcher(injection); 273 mGlobalCallbackDispatcher = null; // If this NPEs before registration, we want to abort. 274 // Implement the IInjectGlobalEvent IInterface. 275 // Since we can't extend multiple IInterface from the same object, instantiate an instance 276 // for our clients. 277 mGlobalEventSession = new IInjectGlobalEvent.Stub() { 278 /** 279 * Simulate a HAL process restart. This method is not included in regular HAL interface, 280 * since the entire process is restarted by sending a signal. 281 * Since we run in-proc, we must offer an explicit restart method. 282 * oneway 283 */ 284 @Override 285 public void triggerRestart() { 286 synchronized (FakeSoundTriggerHal.this.mLock) { 287 if (mIsDead) return; 288 mIsDead = true; 289 mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> 290 cb.onRestarted(this)); 291 mModelSessionMap.clear(); 292 if (mDeathRecipient != null) { 293 final DeathRecipient deathRecipient = mDeathRecipient; 294 ExecutorHolder.CALLBACK_EXECUTOR.execute(() -> { 295 try { 296 deathRecipient.binderDied(FakeSoundTriggerHal.this.asBinder()); 297 } catch (Throwable e) { 298 // We don't expect RemoteException at the moment since we run 299 // in the same process 300 Slog.wtf(TAG, "Callback dispatch threw", e); 301 } 302 }); 303 } 304 } 305 } 306 307 // oneway 308 @Override 309 public void setResourceContention(boolean isResourcesContended, 310 IAcknowledgeEvent callback) { 311 synchronized (FakeSoundTriggerHal.this.mLock) { 312 // oneway, so don't throw on death 313 if (mIsDead) { 314 return; 315 } 316 boolean oldIsResourcesContended = mIsResourceContended; 317 mIsResourceContended = isResourcesContended; 318 // Introducing contention is the only injection which can't be 319 // observed by the ST client. 320 mInjectionDispatcher.wrap((ISoundTriggerInjection unused) -> 321 callback.eventReceived()); 322 if (!mIsResourceContended && oldIsResourcesContended) { 323 mGlobalCallbackDispatcher.wrap((ISoundTriggerHwGlobalCallback cb) -> 324 cb.onResourcesAvailable()); 325 } 326 } 327 } 328 329 // oneway 330 @Override 331 public void triggerOnResourcesAvailable() { 332 synchronized (FakeSoundTriggerHal.this.mLock) { 333 // oneway, so don't throw on death 334 if (mIsDead) return; 335 mGlobalCallbackDispatcher.wrap((ISoundTriggerHwGlobalCallback cb) -> 336 cb.onResourcesAvailable()); 337 } 338 } 339 }; 340 341 // Register the global event injection interface 342 mInjectionDispatcher.wrap((ISoundTriggerInjection cb) 343 -> cb.registerGlobalEventInjection(mGlobalEventSession)); 344 } 345 346 /** 347 * Get the {@link IInjectGlobalEvent} associated with this instance of the STHAL. 348 * Used as a session token, valid until restarted. 349 */ getGlobalEventInjection()350 public IInjectGlobalEvent getGlobalEventInjection() { 351 return mGlobalEventSession; 352 } 353 354 // TODO(b/274467228) we can remove the next three methods when this HAL is moved out-of-proc, 355 // so process restart at death notification is appropriately handled by the binder. 356 @Override linkToDeath(IBinder.DeathRecipient recipient, int flags)357 public void linkToDeath(IBinder.DeathRecipient recipient, int flags) { 358 synchronized (mLock) { 359 if (mDeathRecipient != null) { 360 Slog.wtf(TAG, "Received two death recipients concurrently"); 361 } 362 mDeathRecipient = recipient; 363 } 364 } 365 366 @Override unlinkToDeath(IBinder.DeathRecipient recipient, int flags)367 public boolean unlinkToDeath(IBinder.DeathRecipient recipient, int flags) { 368 synchronized (mLock) { 369 if (mIsDead) return false; 370 if (mDeathRecipient != recipient) { 371 throw new NoSuchElementException(); 372 } 373 mDeathRecipient = null; 374 return true; 375 } 376 } 377 378 // STHAL method overrides to follow 379 @Override getProperties()380 public Properties getProperties() throws RemoteException { 381 synchronized (mLock) { 382 if (mIsDead) throw new DeadObjectException(); 383 Parcel parcel = Parcel.obtain(); 384 try { 385 mProperties.writeToParcel(parcel, 0 /* flags */); 386 parcel.setDataPosition(0); 387 return Properties.CREATOR.createFromParcel(parcel); 388 } finally { 389 parcel.recycle(); 390 } 391 } 392 } 393 394 @Override registerGlobalCallback( ISoundTriggerHwGlobalCallback callback)395 public void registerGlobalCallback( 396 ISoundTriggerHwGlobalCallback callback) throws RemoteException { 397 synchronized (mLock) { 398 if (mIsDead) throw new DeadObjectException(); 399 mGlobalCallbackDispatcher = new GlobalCallbackDispatcher(callback); 400 } 401 } 402 403 @Override loadSoundModel(SoundModel soundModel, ISoundTriggerHwCallback callback)404 public int loadSoundModel(SoundModel soundModel, 405 ISoundTriggerHwCallback callback) throws RemoteException { 406 synchronized (mLock) { 407 if (mIsDead) throw new DeadObjectException(); 408 if (mIsResourceContended || getNumLoadedModelsLocked() == mProperties.maxSoundModels) { 409 throw new ServiceSpecificException(Status.RESOURCE_CONTENTION); 410 } 411 int key = mModelKeyCounter++; 412 ModelSession session = new ModelSession(key, new CallbackDispatcher(callback), false); 413 414 mModelSessionMap.put(key, session); 415 416 mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> 417 cb.onSoundModelLoaded(soundModel, null, session, mGlobalEventSession)); 418 return key; 419 } 420 } 421 422 @Override loadPhraseSoundModel(PhraseSoundModel soundModel, ISoundTriggerHwCallback callback)423 public int loadPhraseSoundModel(PhraseSoundModel soundModel, 424 ISoundTriggerHwCallback callback) throws RemoteException { 425 synchronized (mLock) { 426 if (mIsDead) throw new DeadObjectException(); 427 if (mIsResourceContended || getNumLoadedModelsLocked() == mProperties.maxSoundModels) { 428 throw new ServiceSpecificException(Status.RESOURCE_CONTENTION); 429 } 430 431 int key = mModelKeyCounter++; 432 ModelSession session = new ModelSession(key, new CallbackDispatcher(callback), true); 433 434 mModelSessionMap.put(key, session); 435 436 mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> 437 cb.onSoundModelLoaded(soundModel.common, soundModel.phrases, session, 438 mGlobalEventSession)); 439 return key; 440 } 441 } 442 443 @Override unloadSoundModel(int modelHandle)444 public void unloadSoundModel(int modelHandle) throws RemoteException { 445 synchronized (mLock) { 446 if (mIsDead) throw new DeadObjectException(); 447 ModelSession session = mModelSessionMap.get(modelHandle); 448 if (session == null) { 449 Slog.wtf(TAG, "Attempted to unload model which was never loaded"); 450 } 451 452 if (session.getRecogSession() != null) { 453 Slog.wtf(TAG, "Session unloaded before recog stopped!"); 454 } 455 456 // Session is stale 457 if (session.getIsUnloaded()) return; 458 mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> 459 cb.onSoundModelUnloaded(session)); 460 461 // Notify if we could have denied a previous model due to contention 462 if (getNumLoadedModelsLocked() == (mProperties.maxSoundModels - 1) 463 && !mIsResourceContended) { 464 mGlobalCallbackDispatcher.wrap((ISoundTriggerHwGlobalCallback cb) -> 465 cb.onResourcesAvailable()); 466 } 467 468 } 469 } 470 471 @Override startRecognition(int modelHandle, int deviceHandle, int ioHandle, RecognitionConfig config)472 public void startRecognition(int modelHandle, int deviceHandle, int ioHandle, 473 RecognitionConfig config) throws RemoteException { 474 synchronized (mLock) { 475 if (mIsDead) throw new DeadObjectException(); 476 ModelSession session = mModelSessionMap.get(modelHandle); 477 if (session == null) { 478 Slog.wtf(TAG, "Attempted to start recognition with invalid handle"); 479 } 480 if (mIsResourceContended) { 481 throw new ServiceSpecificException(Status.RESOURCE_CONTENTION); 482 } 483 if (session.getIsUnloaded()) { 484 // TODO(b/274470274) this is a deficiency in the existing HAL API, there is no way 485 // to handle this race gracefully 486 throw new ServiceSpecificException(Status.RESOURCE_CONTENTION); 487 } 488 ModelSession.RecognitionSession recogSession = session.startRecognitionForModel(); 489 490 // TODO(b/274470571) appropriately translate ioHandle to session handle 491 mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> 492 cb.onRecognitionStarted(-1, config, recogSession, session)); 493 } 494 } 495 496 @Override stopRecognition(int modelHandle)497 public void stopRecognition(int modelHandle) throws RemoteException { 498 synchronized (mLock) { 499 if (mIsDead) throw new DeadObjectException(); 500 ModelSession session = mModelSessionMap.get(modelHandle); 501 if (session == null) { 502 Slog.wtf(TAG, "Attempted to stop recognition with invalid handle"); 503 } 504 ModelSession.RecognitionSession recogSession = session.stopRecognitionForModel(); 505 if (recogSession != null) { 506 mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> 507 cb.onRecognitionStopped(recogSession)); 508 } 509 } 510 } 511 512 @Override forceRecognitionEvent(int modelHandle)513 public void forceRecognitionEvent(int modelHandle) throws RemoteException { 514 synchronized (mLock) { 515 if (mIsDead) throw new DeadObjectException(); 516 ModelSession session = mModelSessionMap.get(modelHandle); 517 if (session == null) { 518 Slog.wtf(TAG, "Attempted to force recognition with invalid handle"); 519 } 520 521 // TODO(b/274470274) this is a deficiency in the existing HAL API, we could always 522 // get a force request for an already stopped model. The only thing to do is 523 // drop such a request. 524 if (session.getRecogSession() == null) return; 525 session.forceRecognitionForModel(); 526 } 527 } 528 529 // TODO(b/274470274) this is a deficiency in the existing HAL API, we could always 530 // get model param API requests after model unload. 531 // For now, succeed anyway to maintain fidelity to existing HALs. 532 @Override queryParameter(int modelHandle, int modelParam)533 public @Nullable ModelParameterRange queryParameter(int modelHandle, 534 /** ModelParameter **/ int modelParam) throws RemoteException { 535 synchronized (mLock) { 536 if (mIsDead) throw new DeadObjectException(); 537 ModelSession session = mModelSessionMap.get(modelHandle); 538 if (session == null) { 539 Slog.wtf(TAG, "Attempted to get param with invalid handle"); 540 } 541 } 542 if (modelParam == ModelParameter.THRESHOLD_FACTOR) { 543 ModelParameterRange range = new ModelParameterRange(); 544 range.minInclusive = THRESHOLD_MIN; 545 range.maxInclusive = THRESHOLD_MAX; 546 return range; 547 } else { 548 return null; 549 } 550 } 551 552 @Override getParameter(int modelHandle, int modelParam)553 public int getParameter(int modelHandle, 554 /** ModelParameter **/ int modelParam) throws RemoteException { 555 synchronized (mLock) { 556 if (mIsDead) throw new DeadObjectException(); 557 ModelSession session = mModelSessionMap.get(modelHandle); 558 if (session == null) { 559 Slog.wtf(TAG, "Attempted to get param with invalid handle"); 560 } 561 if (modelParam != ModelParameter.THRESHOLD_FACTOR) { 562 throw new IllegalArgumentException(); 563 } 564 return session.getThresholdFactor(); 565 } 566 } 567 568 @Override setParameter(int modelHandle, int modelParam, int value)569 public void setParameter(int modelHandle, 570 /** ModelParameter **/ int modelParam, int value) throws RemoteException { 571 synchronized (mLock) { 572 if (mIsDead) throw new DeadObjectException(); 573 ModelSession session = mModelSessionMap.get(modelHandle); 574 if (session == null) { 575 Slog.wtf(TAG, "Attempted to get param with invalid handle"); 576 } 577 if ((modelParam == ModelParameter.THRESHOLD_FACTOR) 578 || (value >= THRESHOLD_MIN && value <= THRESHOLD_MAX)) { 579 session.setThresholdFactor(value); 580 } else { 581 throw new IllegalArgumentException(); 582 } 583 mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> 584 cb.onParamSet(modelParam, value, session)); 585 } 586 } 587 588 @Override getInterfaceVersion()589 public int getInterfaceVersion() throws RemoteException { 590 synchronized (mLock) { 591 if (mIsDead) throw new DeadObjectException(); 592 } 593 return super.VERSION; 594 } 595 596 @Override getInterfaceHash()597 public String getInterfaceHash() throws RemoteException { 598 synchronized (mLock) { 599 if (mIsDead) throw new DeadObjectException(); 600 } 601 return super.HASH; 602 } 603 604 // Helpers to follow. 605 @GuardedBy("mLock") getNumLoadedModelsLocked()606 private int getNumLoadedModelsLocked() { 607 int numModels = 0; 608 for (ModelSession session : mModelSessionMap.values()) { 609 if (!session.getIsUnloaded()) { 610 numModels++; 611 } 612 } 613 return numModels; 614 } 615 createDefaultProperties()616 private static Properties createDefaultProperties() { 617 Properties properties = new Properties(); 618 properties.implementor = "android"; 619 properties.description = "AOSP fake STHAL"; 620 properties.version = 1; 621 properties.uuid = "00000001-0002-0003-0004-deadbeefabcd"; 622 properties.supportedModelArch = ISoundTriggerInjection.FAKE_HAL_ARCH; 623 properties.maxSoundModels = 8; 624 properties.maxKeyPhrases = 2; 625 properties.maxUsers = 2; 626 properties.recognitionModes = RecognitionMode.VOICE_TRIGGER 627 | RecognitionMode.GENERIC_TRIGGER; 628 properties.captureTransition = true; 629 // This is actually not respected, since there is no real AudioRecord 630 properties.maxBufferMs = 5000; 631 properties.concurrentCapture = true; 632 properties.triggerInEvent = false; 633 properties.powerConsumptionMw = 0; 634 properties.audioCapabilities = 0; 635 return properties; 636 } 637 createDefaultEvent( int status)638 private static RecognitionEvent createDefaultEvent( 639 /** RecognitionStatus **/ int status) { 640 RecognitionEvent event = new RecognitionEvent(); 641 // Overwrite the event appropriately. 642 event.status = status; 643 event.type = SoundModelType.GENERIC; 644 // TODO(b/274466981) make this configurable. 645 // For now, some plausible defaults 646 event.captureAvailable = true; 647 event.captureDelayMs = 50; 648 event.capturePreambleMs = 200; 649 event.triggerInData = false; 650 event.audioConfig = null; // Nullable within AIDL 651 event.data = new byte[0]; 652 // We don't support recognition restart for now 653 event.recognitionStillActive = false; 654 return event; 655 } 656 createDefaultKeyphraseEvent( int status)657 private static PhraseRecognitionEvent createDefaultKeyphraseEvent( 658 /**RecognitionStatus **/ int status) { 659 RecognitionEvent event = createDefaultEvent(status); 660 event.type = SoundModelType.KEYPHRASE; 661 PhraseRecognitionEvent phraseEvent = new PhraseRecognitionEvent(); 662 phraseEvent.common = event; 663 phraseEvent.phraseExtras = new PhraseRecognitionExtra[0]; 664 return phraseEvent; 665 } 666 667 // Helper classes to dispatch oneway calls to the appropriate callback interfaces to follow. 668 private static class CallbackDispatcher { 669 CallbackDispatcher(ISoundTriggerHwCallback callback)670 private CallbackDispatcher(ISoundTriggerHwCallback callback) { 671 mCallback = callback; 672 } 673 wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerHwCallback> command)674 private void wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerHwCallback> command) { 675 ExecutorHolder.CALLBACK_EXECUTOR.execute(() -> { 676 try { 677 command.accept(mCallback); 678 } catch (Throwable e) { 679 Slog.wtf(TAG, "Callback dispatch threw", e); 680 } 681 }); 682 } 683 684 private final ISoundTriggerHwCallback mCallback; 685 } 686 687 private static class GlobalCallbackDispatcher { 688 GlobalCallbackDispatcher(ISoundTriggerHwGlobalCallback callback)689 private GlobalCallbackDispatcher(ISoundTriggerHwGlobalCallback callback) { 690 mCallback = callback; 691 } 692 wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerHwGlobalCallback> command)693 private void wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerHwGlobalCallback> command) { 694 ExecutorHolder.CALLBACK_EXECUTOR.execute(() -> { 695 try { 696 command.accept(mCallback); 697 } catch (Throwable e) { 698 // We don't expect RemoteException at the moment since we run 699 // in the same process 700 Slog.wtf(TAG, "Callback dispatch threw", e); 701 } 702 }); 703 } 704 705 private final ISoundTriggerHwGlobalCallback mCallback; 706 } 707 708 private static class InjectionDispatcher { 709 InjectionDispatcher(ISoundTriggerInjection injection)710 private InjectionDispatcher(ISoundTriggerInjection injection) { 711 mInjection = injection; 712 } 713 wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerInjection> command)714 private void wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerInjection> command) { 715 ExecutorHolder.INJECTION_EXECUTOR.execute(() -> { 716 try { 717 command.accept(mInjection); 718 } catch (Throwable e) { 719 // We don't expect RemoteException at the moment since we run 720 // in the same process 721 Slog.wtf(TAG, "Callback dispatch threw", e); 722 } 723 }); 724 } 725 726 private final ISoundTriggerInjection mInjection; 727 } 728 } 729