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 android.media.soundtrigger; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.RequiresPermission; 23 import android.annotation.TestApi; 24 import android.hardware.soundtrigger.ConversionUtil; 25 import android.hardware.soundtrigger.SoundTrigger; 26 import android.media.soundtrigger_middleware.IAcknowledgeEvent; 27 import android.media.soundtrigger_middleware.IInjectGlobalEvent; 28 import android.media.soundtrigger_middleware.IInjectModelEvent; 29 import android.media.soundtrigger_middleware.IInjectRecognitionEvent; 30 import android.media.soundtrigger_middleware.ISoundTriggerInjection; 31 import android.os.IBinder; 32 import android.os.RemoteException; 33 34 import com.android.internal.annotations.GuardedBy; 35 import com.android.internal.app.ISoundTriggerService; 36 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.HashMap; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Objects; 43 import java.util.UUID; 44 import java.util.concurrent.CountDownLatch; 45 import java.util.concurrent.Executor; 46 import java.util.function.Consumer; 47 48 /** 49 * Used to inject/observe events when using a fake SoundTrigger HAL for test purposes. 50 * Created by {@link SoundTriggerManager#getInjection(Executor, GlobalCallback)}. 51 * Only one instance of this class is valid at any given time, old instances will be delivered 52 * {@link GlobalCallback#onPreempted()}. 53 * @hide 54 */ 55 @TestApi 56 public final class SoundTriggerInstrumentation { 57 58 private final Object mLock = new Object(); 59 @GuardedBy("mLock") 60 private IInjectGlobalEvent mInjectGlobalEvent = null; 61 62 @GuardedBy("mLock") 63 private Map<IBinder, ModelSession> mModelSessionMap = new HashMap<>(); 64 @GuardedBy("mLock") 65 private Map<IBinder, RecognitionSession> mRecognitionSessionMap = new HashMap<>(); 66 @GuardedBy("mLock") 67 private IBinder mClientToken = null; 68 69 private final ISoundTriggerService mService; 70 71 private final GlobalCallback mClientCallback; 72 private final Executor mGlobalCallbackExecutor; 73 74 /** 75 * Callback interface for un-sessioned events observed from the fake STHAL. 76 * Registered upon construction of {@link SoundTriggerInstrumentation} 77 * @hide 78 */ 79 @TestApi 80 public interface GlobalCallback { 81 /** 82 * Called when the created {@link SoundTriggerInstrumentation} object is invalidated 83 * by another client creating an {@link SoundTriggerInstrumentation} to instrument the 84 * fake STHAL. Only one client may inject at a time. 85 * All sessions are invalidated, no further events will be received, and no 86 * injected events will be delivered. 87 */ onPreempted()88 default void onPreempted() {} 89 /** 90 * Called when the STHAL has been restarted by the framework, due to unexpected 91 * error conditions. 92 * Not called when {@link SoundTriggerInstrumentation#triggerRestart()} is injected. 93 */ onRestarted()94 default void onRestarted() {} 95 /** 96 * Called when the framework detaches from the fake HAL. 97 * This is not transmitted to real HALs, but it indicates that the 98 * framework has flushed its global state. 99 */ onFrameworkDetached()100 default void onFrameworkDetached() {} 101 /** 102 * Called when a client application attaches to the framework. 103 * This is not transmitted to real HALs, but it represents the state of 104 * the framework. 105 */ onClientAttached()106 default void onClientAttached() {} 107 /** 108 * Called when a client application detaches from the framework. 109 * This is not transmitted to real HALs, but it represents the state of 110 * the framework. 111 */ onClientDetached()112 default void onClientDetached() {} 113 /** 114 * Called when the fake HAL receives a model load from the framework. 115 * @param modelSession - A session which exposes additional injection 116 * functionality associated with the newly loaded 117 * model. See {@link ModelSession}. 118 */ onModelLoaded(@onNull ModelSession modelSession)119 void onModelLoaded(@NonNull ModelSession modelSession); 120 } 121 122 /** 123 * Callback for HAL events related to a loaded model. Register with 124 * {@link ModelSession#setModelCallback(Executor, ModelCallback)} 125 * Note, callbacks will not be delivered for events triggered by the injection. 126 * @hide 127 */ 128 @TestApi 129 public interface ModelCallback { 130 /** 131 * Called when the model associated with the {@link ModelSession} this callback 132 * was registered for was unloaded by the framework. 133 */ onModelUnloaded()134 default void onModelUnloaded() {} 135 /** 136 * Called when the model associated with the {@link ModelSession} this callback 137 * was registered for receives a set parameter call from the framework. 138 * @param param - Parameter being set. 139 * See {@link SoundTrigger.ModelParamTypes} 140 * @param value - Value the model parameter was set to. 141 */ onParamSet(@oundTrigger.ModelParamTypes int param, int value)142 default void onParamSet(@SoundTrigger.ModelParamTypes int param, int value) {} 143 /** 144 * Called when the model associated with the {@link ModelSession} this callback 145 * was registered for receives a recognition start request. 146 * @param recognitionSession - A session which exposes additional injection 147 * functionality associated with the newly started 148 * recognition. See {@link RecognitionSession} 149 */ onRecognitionStarted(@onNull RecognitionSession recognitionSession)150 void onRecognitionStarted(@NonNull RecognitionSession recognitionSession); 151 } 152 153 /** 154 * Callback for HAL events related to a started recognition. Register with 155 * {@link RecognitionSession#setRecognitionCallback(Executor, RecognitionCallback)} 156 * Note, callbacks will not be delivered for events triggered by the injection. 157 * @hide 158 */ 159 @TestApi 160 public interface RecognitionCallback { 161 /** 162 * Called when the recognition associated with the {@link RecognitionSession} this 163 * callback was registered for was stopped by the framework. 164 */ onRecognitionStopped()165 void onRecognitionStopped(); 166 } 167 168 /** 169 * Session associated with a loaded model in the fake STHAL. 170 * Can be used to query details about the loaded model, register a callback for future 171 * model events, or trigger HAL events associated with a loaded model. 172 * This session is invalid once the model is unloaded, caused by a 173 * {@link ModelSession#triggerUnloadModel()}, 174 * the client unloading recognition, or if a {@link GlobalCallback#onRestarted()} is 175 * received. 176 * Further injections on an invalidated session will not be respected, and no future 177 * callbacks will be delivered. 178 * @hide 179 */ 180 @TestApi 181 public class ModelSession { 182 183 /** 184 * Trigger the HAL to preemptively unload the model associated with this session. 185 * Typically occurs when a higher priority model is loaded which utilizes the same 186 * resources. 187 */ triggerUnloadModel()188 public void triggerUnloadModel() { 189 synchronized (SoundTriggerInstrumentation.this.mLock) { 190 try { 191 mInjectModelEvent.triggerUnloadModel(); 192 } catch (RemoteException e) { 193 throw e.rethrowFromSystemServer(); 194 } 195 mModelSessionMap.remove(mInjectModelEvent.asBinder()); 196 } 197 } 198 199 /** 200 * Get the {@link SoundTriggerManager.Model} associated with this session. 201 * @return - The model associated with this session. 202 */ getSoundModel()203 public @NonNull SoundTriggerManager.Model getSoundModel() { 204 return mModel; 205 } 206 207 /** 208 * Get the list of {@link SoundTrigger.Keyphrase} associated with this session. 209 * @return - The keyphrases associated with this session. 210 */ getPhrases()211 public @NonNull List<SoundTrigger.Keyphrase> getPhrases() { 212 if (mPhrases == null) { 213 return new ArrayList<>(); 214 } else { 215 return new ArrayList<>(Arrays.asList(mPhrases)); 216 } 217 } 218 219 /** 220 * Get whether this model is of keyphrase type. 221 * @return - true if the model is a keyphrase model, false otherwise 222 */ isKeyphrase()223 public boolean isKeyphrase() { 224 return (mPhrases != null); 225 } 226 227 /** 228 * Registers the model callback associated with this session. Events associated 229 * with this model session will be reported via this callback. 230 * See {@link ModelCallback} 231 * @param executor - Executor which the callback is dispatched on 232 * @param callback - Model callback for reporting model session events. 233 */ setModelCallback(@onNull @allbackExecutor Executor executor, @NonNull ModelCallback callback)234 public void setModelCallback(@NonNull @CallbackExecutor Executor executor, @NonNull 235 ModelCallback callback) { 236 Objects.requireNonNull(callback); 237 Objects.requireNonNull(executor); 238 synchronized (SoundTriggerInstrumentation.this.mLock) { 239 if (mModelCallback == null) { 240 for (var droppedConsumer : mDroppedConsumerList) { 241 executor.execute(() -> droppedConsumer.accept(callback)); 242 } 243 mDroppedConsumerList.clear(); 244 } 245 mModelCallback = callback; 246 mModelExecutor = executor; 247 } 248 } 249 250 /** 251 * Clear the model callback associated with this session, if any has been 252 * set by {@link #setModelCallback(Executor, ModelCallback)}. 253 */ clearModelCallback()254 public void clearModelCallback() { 255 synchronized (SoundTriggerInstrumentation.this.mLock) { 256 mModelCallback = null; 257 mModelExecutor = null; 258 } 259 } 260 ModelSession(SoundModel model, Phrase[] phrases, IInjectModelEvent injection)261 private ModelSession(SoundModel model, Phrase[] phrases, 262 IInjectModelEvent injection) { 263 mModel = SoundTriggerManager.Model.create(UUID.fromString(model.uuid), 264 UUID.fromString(model.vendorUuid), 265 ConversionUtil.sharedMemoryToByteArray(model.data, model.dataSize)); 266 if (phrases != null) { 267 mPhrases = new SoundTrigger.Keyphrase[phrases.length]; 268 int i = 0; 269 for (var phrase : phrases) { 270 mPhrases[i++] = ConversionUtil.aidl2apiPhrase(phrase); 271 } 272 } else { 273 mPhrases = null; 274 } 275 mInjectModelEvent = injection; 276 } 277 wrap(Consumer<ModelCallback> consumer)278 private void wrap(Consumer<ModelCallback> consumer) { 279 synchronized (SoundTriggerInstrumentation.this.mLock) { 280 if (mModelCallback != null) { 281 final ModelCallback callback = mModelCallback; 282 mModelExecutor.execute(() -> consumer.accept(callback)); 283 } else { 284 mDroppedConsumerList.add(consumer); 285 } 286 } 287 } 288 289 private final SoundTriggerManager.Model mModel; 290 private final SoundTrigger.Keyphrase[] mPhrases; 291 private final IInjectModelEvent mInjectModelEvent; 292 293 @GuardedBy("SoundTriggerInstrumentation.this.mLock") 294 private ModelCallback mModelCallback = null; 295 @GuardedBy("SoundTriggerInstrumentation.this.mLock") 296 private Executor mModelExecutor = null; 297 @GuardedBy("SoundTriggerInstrumentation.this.mLock") 298 private final List<Consumer<ModelCallback>> mDroppedConsumerList = new ArrayList<>(); 299 } 300 301 /** 302 * Session associated with a recognition start in the fake STHAL. 303 * Can be used to get information about the started recognition, register a callback 304 * for future events associated with this recognition, and triggering 305 * recognition events or aborts. 306 * This session is invalid once the recognition is stopped, caused by a 307 * {@link RecognitionSession#triggerAbortRecognition()}, 308 * {@link RecognitionSession#triggerRecognitionEvent(byte[], List)}, 309 * the client stopping recognition, or any operation which invalidates the 310 * {@link ModelSession} which the session was created from. 311 * Further injections on an invalidated session will not be respected, and no future 312 * callbacks will be delivered. 313 * @hide 314 */ 315 @TestApi 316 public class RecognitionSession { 317 318 /** 319 * Get an integer token representing the audio session associated with this 320 * recognition in the STHAL. 321 * @return - The session token. 322 */ getAudioSession()323 public int getAudioSession() { 324 return mAudioSession; 325 } 326 327 /** 328 * Get the recognition config used to start this recognition. 329 * @return - The config passed to the HAL for startRecognition. 330 */ getRecognitionConfig()331 public @NonNull SoundTrigger.RecognitionConfig getRecognitionConfig() { 332 return mRecognitionConfig; 333 } 334 335 /** 336 * Trigger a recognition in the fake STHAL. 337 * @param data - The opaque data buffer included in the recognition event. 338 * @param phraseExtras - Keyphrase metadata included in the event. The 339 * event must include metadata for the keyphrase id 340 * associated with this model to be received by the 341 * client application. 342 */ triggerRecognitionEvent(@onNull byte[] data, @Nullable List<SoundTrigger.KeyphraseRecognitionExtra> phraseExtras)343 public void triggerRecognitionEvent(@NonNull byte[] data, @Nullable 344 List<SoundTrigger.KeyphraseRecognitionExtra> phraseExtras) { 345 PhraseRecognitionExtra[] converted = null; 346 if (phraseExtras != null) { 347 converted = new PhraseRecognitionExtra[phraseExtras.size()]; 348 int i = 0; 349 for (var phraseExtra : phraseExtras) { 350 converted[i++] = ConversionUtil.api2aidlPhraseRecognitionExtra(phraseExtra); 351 } 352 } 353 synchronized (SoundTriggerInstrumentation.this.mLock) { 354 mRecognitionSessionMap.remove(mInjectRecognitionEvent.asBinder()); 355 try { 356 mInjectRecognitionEvent.triggerRecognitionEvent(data, converted); 357 } catch (RemoteException e) { 358 throw e.rethrowFromSystemServer(); 359 } 360 } 361 } 362 363 /** 364 * Trigger an abort recognition event in the fake HAL. This represents a 365 * preemptive ending of the recognition session by the HAL, despite no 366 * recognition detection. Typically occurs during contention for microphone 367 * usage, or if model limits are hit. 368 * See {@link SoundTriggerInstrumentation#setResourceContention(boolean)} to block 369 * subsequent downward calls for contention reasons. 370 */ triggerAbortRecognition()371 public void triggerAbortRecognition() { 372 synchronized (SoundTriggerInstrumentation.this.mLock) { 373 mRecognitionSessionMap.remove(mInjectRecognitionEvent.asBinder()); 374 try { 375 mInjectRecognitionEvent.triggerAbortRecognition(); 376 } catch (RemoteException e) { 377 throw e.rethrowFromSystemServer(); 378 } 379 } 380 } 381 382 /** 383 * Registers the recognition callback associated with this session. Events associated 384 * with this recognition session will be reported via this callback. 385 * See {@link RecognitionCallback} 386 * @param executor - Executor which the callback is dispatched on 387 * @param callback - Recognition callback for reporting recognition session events. 388 */ setRecognitionCallback(@onNull @allbackExecutor Executor executor, @NonNull RecognitionCallback callback)389 public void setRecognitionCallback(@NonNull @CallbackExecutor Executor executor, 390 @NonNull RecognitionCallback callback) { 391 Objects.requireNonNull(callback); 392 Objects.requireNonNull(executor); 393 synchronized (SoundTriggerInstrumentation.this.mLock) { 394 if (mRecognitionCallback == null) { 395 for (var droppedConsumer : mDroppedConsumerList) { 396 executor.execute(() -> droppedConsumer.accept(callback)); 397 } 398 mDroppedConsumerList.clear(); 399 } 400 mRecognitionCallback = callback; 401 mRecognitionExecutor = executor; 402 403 } 404 } 405 406 /** 407 * Clear the recognition callback associated with this session, if any has been 408 * set by {@link #setRecognitionCallback(Executor, RecognitionCallback)}. 409 */ clearRecognitionCallback()410 public void clearRecognitionCallback() { 411 synchronized (SoundTriggerInstrumentation.this.mLock) { 412 mRecognitionCallback = null; 413 mRecognitionExecutor = null; 414 } 415 } 416 RecognitionSession(int audioSession, RecognitionConfig recognitionConfig, IInjectRecognitionEvent injectRecognitionEvent)417 private RecognitionSession(int audioSession, 418 RecognitionConfig recognitionConfig, 419 IInjectRecognitionEvent injectRecognitionEvent) { 420 mAudioSession = audioSession; 421 mRecognitionConfig = ConversionUtil.aidl2apiRecognitionConfig(recognitionConfig); 422 mInjectRecognitionEvent = injectRecognitionEvent; 423 } 424 wrap(Consumer<RecognitionCallback> consumer)425 private void wrap(Consumer<RecognitionCallback> consumer) { 426 synchronized (SoundTriggerInstrumentation.this.mLock) { 427 if (mRecognitionCallback != null) { 428 final RecognitionCallback callback = mRecognitionCallback; 429 mRecognitionExecutor.execute(() -> consumer.accept(callback)); 430 } else { 431 mDroppedConsumerList.add(consumer); 432 } 433 } 434 } 435 436 private final int mAudioSession; 437 private final SoundTrigger.RecognitionConfig mRecognitionConfig; 438 private final IInjectRecognitionEvent mInjectRecognitionEvent; 439 440 @GuardedBy("SoundTriggerInstrumentation.this.mLock") 441 private Executor mRecognitionExecutor = null; 442 @GuardedBy("SoundTriggerInstrumentation.this.mLock") 443 private RecognitionCallback mRecognitionCallback = null; 444 @GuardedBy("SoundTriggerInstrumentation.this.mLock") 445 private final List<Consumer<RecognitionCallback>> mDroppedConsumerList = new ArrayList<>(); 446 } 447 448 // Implementation of injection interface passed to the HAL. 449 // This class will re-associate events received on this callback interface 450 // with sessions, to avoid staleness issues. 451 private class Injection extends ISoundTriggerInjection.Stub { 452 @Override registerGlobalEventInjection(IInjectGlobalEvent globalInjection)453 public void registerGlobalEventInjection(IInjectGlobalEvent globalInjection) { 454 synchronized (SoundTriggerInstrumentation.this.mLock) { 455 mInjectGlobalEvent = globalInjection; 456 } 457 } 458 459 @Override onSoundModelLoaded(SoundModel model, @Nullable Phrase[] phrases, IInjectModelEvent modelInjection, IInjectGlobalEvent globalSession)460 public void onSoundModelLoaded(SoundModel model, @Nullable Phrase[] phrases, 461 IInjectModelEvent modelInjection, IInjectGlobalEvent globalSession) { 462 synchronized (SoundTriggerInstrumentation.this.mLock) { 463 if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return; 464 ModelSession modelSession = new ModelSession(model, phrases, modelInjection); 465 mModelSessionMap.put(modelInjection.asBinder(), modelSession); 466 mGlobalCallbackExecutor.execute(() -> mClientCallback.onModelLoaded(modelSession)); 467 } 468 } 469 470 @Override onSoundModelUnloaded(IInjectModelEvent modelSession)471 public void onSoundModelUnloaded(IInjectModelEvent modelSession) { 472 synchronized (SoundTriggerInstrumentation.this.mLock) { 473 ModelSession clientModelSession = mModelSessionMap.remove(modelSession.asBinder()); 474 if (clientModelSession == null) return; 475 clientModelSession.wrap((ModelCallback cb) -> cb.onModelUnloaded()); 476 } 477 } 478 479 @Override onRecognitionStarted(int audioSessionHandle, RecognitionConfig config, IInjectRecognitionEvent recognitionInjection, IInjectModelEvent modelSession)480 public void onRecognitionStarted(int audioSessionHandle, RecognitionConfig config, 481 IInjectRecognitionEvent recognitionInjection, IInjectModelEvent modelSession) { 482 synchronized (SoundTriggerInstrumentation.this.mLock) { 483 ModelSession clientModelSession = mModelSessionMap.get(modelSession.asBinder()); 484 if (clientModelSession == null) return; 485 RecognitionSession recogSession = new RecognitionSession( 486 audioSessionHandle, config, recognitionInjection); 487 mRecognitionSessionMap.put(recognitionInjection.asBinder(), recogSession); 488 clientModelSession.wrap((ModelCallback cb) -> 489 cb.onRecognitionStarted(recogSession)); 490 } 491 } 492 493 @Override onRecognitionStopped(IInjectRecognitionEvent recognitionSession)494 public void onRecognitionStopped(IInjectRecognitionEvent recognitionSession) { 495 synchronized (SoundTriggerInstrumentation.this.mLock) { 496 RecognitionSession clientRecognitionSession = 497 mRecognitionSessionMap.remove(recognitionSession.asBinder()); 498 if (clientRecognitionSession == null) return; 499 clientRecognitionSession.wrap((RecognitionCallback cb) 500 -> cb.onRecognitionStopped()); 501 } 502 } 503 504 @Override onParamSet(int modelParam, int value, IInjectModelEvent modelSession)505 public void onParamSet(int modelParam, int value, IInjectModelEvent modelSession) { 506 synchronized (SoundTriggerInstrumentation.this.mLock) { 507 ModelSession clientModelSession = mModelSessionMap.get(modelSession.asBinder()); 508 if (clientModelSession == null) return; 509 clientModelSession.wrap((ModelCallback cb) -> cb.onParamSet(modelParam, value)); 510 } 511 } 512 513 514 @Override onRestarted(IInjectGlobalEvent globalSession)515 public void onRestarted(IInjectGlobalEvent globalSession) { 516 synchronized (SoundTriggerInstrumentation.this.mLock) { 517 if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return; 518 mRecognitionSessionMap.clear(); 519 mModelSessionMap.clear(); 520 mGlobalCallbackExecutor.execute(() -> mClientCallback.onRestarted()); 521 } 522 } 523 524 @Override onFrameworkDetached(IInjectGlobalEvent globalSession)525 public void onFrameworkDetached(IInjectGlobalEvent globalSession) { 526 synchronized (SoundTriggerInstrumentation.this.mLock) { 527 if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return; 528 mGlobalCallbackExecutor.execute(() -> mClientCallback.onFrameworkDetached()); 529 } 530 } 531 532 @Override onClientAttached(IBinder token, IInjectGlobalEvent globalSession)533 public void onClientAttached(IBinder token, IInjectGlobalEvent globalSession) { 534 synchronized (SoundTriggerInstrumentation.this.mLock) { 535 if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return; 536 mClientToken = token; 537 mGlobalCallbackExecutor.execute(() -> mClientCallback.onClientAttached()); 538 } 539 } 540 541 @Override onClientDetached(IBinder token)542 public void onClientDetached(IBinder token) { 543 synchronized (SoundTriggerInstrumentation.this.mLock) { 544 if (token != mClientToken) return; 545 mClientToken = null; 546 mGlobalCallbackExecutor.execute(() -> mClientCallback.onClientDetached()); 547 } 548 } 549 550 @Override onPreempted()551 public void onPreempted() { 552 // This is always valid, independent of session 553 mGlobalCallbackExecutor.execute(() -> mClientCallback.onPreempted()); 554 // Callbacks will no longer be delivered, and injection will be silently dropped. 555 } 556 } 557 558 /** 559 * @hide 560 */ 561 @RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) SoundTriggerInstrumentation(ISoundTriggerService service, @CallbackExecutor @NonNull Executor executor, @NonNull GlobalCallback callback)562 public SoundTriggerInstrumentation(ISoundTriggerService service, 563 @CallbackExecutor @NonNull Executor executor, 564 @NonNull GlobalCallback callback) { 565 mClientCallback = Objects.requireNonNull(callback); 566 mGlobalCallbackExecutor = Objects.requireNonNull(executor); 567 mService = service; 568 try { 569 service.attachInjection(new Injection()); 570 } catch (RemoteException e) { 571 e.rethrowFromSystemServer(); 572 } 573 } 574 575 /** 576 * Simulate a HAL restart, typically caused by the framework on an unexpected error, 577 * or a restart of the core audio HAL. 578 * Application sessions will be detached, and all state will be cleared. The framework 579 * will re-attach to the HAL following restart. 580 * @hide 581 */ 582 @TestApi triggerRestart()583 public void triggerRestart() { 584 synchronized (mLock) { 585 if (mInjectGlobalEvent == null) { 586 throw new IllegalStateException( 587 "Attempted to trigger HAL restart before registration"); 588 } 589 try { 590 mInjectGlobalEvent.triggerRestart(); 591 } catch (RemoteException e) { 592 throw e.rethrowFromSystemServer(); 593 } 594 } 595 } 596 597 /** 598 * Trigger a resource available callback from the fake SoundTrigger HAL to the framework. 599 * This callback notifies the framework that methods which previously failed due to 600 * resource contention may now succeed. 601 * @hide 602 */ 603 @TestApi triggerOnResourcesAvailable()604 public void triggerOnResourcesAvailable() { 605 synchronized (mLock) { 606 if (mInjectGlobalEvent == null) { 607 throw new IllegalStateException( 608 "Attempted to trigger HAL resources available before registration"); 609 } 610 try { 611 mInjectGlobalEvent.triggerOnResourcesAvailable(); 612 } catch (RemoteException e) { 613 throw e.rethrowFromSystemServer(); 614 } 615 } 616 } 617 618 /** 619 * Simulate resource contention, similar to when HAL which does not 620 * support concurrent capture opens a capture stream, or when a HAL 621 * has reached its maximum number of models. 622 * Subsequent model loads and recognition starts will gracefully error. 623 * Since this call does not trigger a callback through the framework, the 624 * call will block until the fake HAL has acknowledged the state change. 625 * @param isResourceContended - true to enable contention, false to return 626 * to normal functioning. 627 * @hide 628 */ 629 @TestApi setResourceContention(boolean isResourceContended)630 public void setResourceContention(boolean isResourceContended) { 631 synchronized (mLock) { 632 if (mInjectGlobalEvent == null) { 633 throw new IllegalStateException("Injection interface not set up"); 634 } 635 IInjectGlobalEvent current = mInjectGlobalEvent; 636 final CountDownLatch signal = new CountDownLatch(1); 637 try { 638 current.setResourceContention(isResourceContended, new IAcknowledgeEvent.Stub() { 639 @Override 640 public void eventReceived() { 641 signal.countDown(); 642 } 643 }); 644 } catch (RemoteException e) { 645 throw e.rethrowFromSystemServer(); 646 } 647 648 // Block until we get a callback from the service that our request was serviced. 649 try { 650 // Rely on test timeout if we don't get a response. 651 signal.await(); 652 } catch (InterruptedException e) { 653 throw new RuntimeException(e); 654 } 655 } 656 } 657 658 /** 659 * Simulate a phone call for {@link com.android.server.soundtrigger.SoundTriggerService}. 660 * If the phone call state changes, the service will be notified to respond. 661 * The service should pause recognition for the duration of the call. 662 * 663 * @param isInPhoneCall - {@code true} to cause the SoundTriggerService to 664 * see the phone call state as off-hook. {@code false} to cause the service to 665 * see the state as normal. 666 * @hide 667 */ 668 @TestApi setInPhoneCallState(boolean isInPhoneCall)669 public void setInPhoneCallState(boolean isInPhoneCall) { 670 try { 671 mService.setInPhoneCallState(isInPhoneCall); 672 } catch (RemoteException e) { 673 throw e.rethrowFromSystemServer(); 674 } 675 } 676 } 677 678