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