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