diff options
author | 2023-02-28 18:47:16 -0800 | |
---|---|---|
committer | 2023-03-26 18:31:03 -0700 | |
commit | 890b2b360129e3a6d47b945b14e64e2635433573 (patch) | |
tree | 685adbd89fcdef246ee50346ae94147297a18491 | |
parent | 290172443ab77336af0b49df4e84dbd6cc76f92f (diff) |
Implement fake STHAL
- Add injection attach method to middleware service
- Add Fake Hal factory, which observes framework detach,
client attach and detach, above the STHAL
- Implement a same-proc ST3 HAL, which allows for observation
and injection via a ISoundTriggerInjection
- Override default restart runnable, death listener behavior to adapt
for same-proc HAL
- Implement SoundTriggerInjection, which facades over injection client
attach/premption with a persistent injection interface.
- Add observation methods to ISoundTriggerHal and implementors to
deliver to the injection instance
Test: Compiles
Bug: 271197938
Change-Id: I11561672bd3b4dc28d4fd158346dfa8560d97351
13 files changed, 1162 insertions, 8 deletions
diff --git a/media/aidl/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl b/media/aidl/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl index d1126b9006e0..531b3ae0c230 100644 --- a/media/aidl/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl +++ b/media/aidl/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package android.media.soundtrigger_middleware; import android.media.permission.Identity; -import android.media.soundtrigger_middleware.ISoundTriggerModule; import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerInjection; +import android.media.soundtrigger_middleware.ISoundTriggerModule; import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; /** @@ -86,4 +88,12 @@ interface ISoundTriggerMiddlewareService { in Identity middlemanIdentity, in Identity originatorIdentity, ISoundTriggerCallback callback); + + /** + * Attach an injection interface interface to the ST mock HAL. + * See {@link ISoundTriggerInjection} for injection details. + * If another client attaches, this session will be pre-empted. + */ + void attachFakeHalInjection(ISoundTriggerInjection injection); + } diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/FakeHalFactory.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/FakeHalFactory.java new file mode 100644 index 000000000000..badda8e22ff0 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/FakeHalFactory.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.media.soundtrigger_middleware.IInjectGlobalEvent; +import android.media.soundtrigger_middleware.ISoundTriggerInjection; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.server.soundtrigger_middleware.FakeSoundTriggerHal.ExecutorHolder; + + +/** + * Alternate HAL factory which constructs {@link FakeSoundTriggerHal} with addition hooks to + * observe framework events. + */ +class FakeHalFactory implements HalFactory { + + private static final String TAG = "FakeHalFactory"; + private final ISoundTriggerInjection mInjection; + + FakeHalFactory(ISoundTriggerInjection injection) { + mInjection = injection; + } + + /** + * We override the methods below at the {@link ISoundTriggerHal} level, because + * they do not represent real HAL events, rather, they are framework exclusive. + * So, we intercept them here and report them to the injection interface. + */ + @Override + public ISoundTriggerHal create() { + final FakeSoundTriggerHal hal = new FakeSoundTriggerHal(mInjection); + final IInjectGlobalEvent session = hal.getGlobalEventInjection(); + // The fake hal is a ST3 HAL implementation. + final ISoundTriggerHal wrapper = new SoundTriggerHw3Compat(hal, + /* reboot runnable */ () -> { + try { + session.triggerRestart(); + } + catch (RemoteException e) { + Slog.wtf(TAG, "Unexpected RemoteException from same process"); + } + }) { + @Override + public void detach() { + ExecutorHolder.INJECTION_EXECUTOR.execute(() -> { + try { + mInjection.onFrameworkDetached(session); + } catch (RemoteException e) { + Slog.wtf(TAG, "Unexpected RemoteException from same process"); + } + }); + } + + @Override + public void clientAttached(IBinder token) { + ExecutorHolder.INJECTION_EXECUTOR.execute(() -> { + try { + mInjection.onClientAttached(token, session); + } catch (RemoteException e) { + Slog.wtf(TAG, "Unexpected RemoteException from same process"); + } + }); + } + + @Override + public void clientDetached(IBinder token) { + ExecutorHolder.INJECTION_EXECUTOR.execute(() -> { + try { + mInjection.onClientDetached(token); + } catch (RemoteException e) { + Slog.wtf(TAG, "Unexpected RemoteException from same process"); + } + }); + } + }; + return wrapper; + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/FakeSoundTriggerHal.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/FakeSoundTriggerHal.java new file mode 100644 index 000000000000..86c4bbfe56b8 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/FakeSoundTriggerHal.java @@ -0,0 +1,712 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.Nullable; +import android.hardware.soundtrigger3.ISoundTriggerHw; +import android.hardware.soundtrigger3.ISoundTriggerHwCallback; +import android.hardware.soundtrigger3.ISoundTriggerHwGlobalCallback; +import android.media.soundtrigger.ModelParameter; +import android.media.soundtrigger.ModelParameterRange; +import android.media.soundtrigger.PhraseRecognitionEvent; +import android.media.soundtrigger.PhraseRecognitionExtra; +import android.media.soundtrigger.PhraseSoundModel; +import android.media.soundtrigger.Properties; +import android.media.soundtrigger.RecognitionConfig; +import android.media.soundtrigger.RecognitionEvent; +import android.media.soundtrigger.RecognitionMode; +import android.media.soundtrigger.RecognitionStatus; +import android.media.soundtrigger.SoundModel; +import android.media.soundtrigger.SoundModelType; +import android.media.soundtrigger.Status; +import android.media.soundtrigger_middleware.IAcknowledgeEvent; +import android.media.soundtrigger_middleware.IInjectGlobalEvent; +import android.media.soundtrigger_middleware.IInjectModelEvent; +import android.media.soundtrigger_middleware.IInjectRecognitionEvent; +import android.media.soundtrigger_middleware.ISoundTriggerInjection; +import android.os.DeadObjectException; +import android.os.IBinder; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.FunctionalUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + + +/** + * Fake HAL implementation, which offers injection via + * {@link ISoundTriggerInjection}. + * Since this is a test interface, upon unexpected operations from the framework, + * we will abort. + */ +public class FakeSoundTriggerHal extends ISoundTriggerHw.Stub { + private static final String TAG = "FakeSoundTriggerHal"; + + // Fake values for valid model param range + private static final int THRESHOLD_MIN = -10; + private static final int THRESHOLD_MAX = 10; + + // Logically const + private final Object mLock = new Object(); + private final Properties mProperties; + + // These cannot be injected, since we rely on: + // 1) Serialization + // 2) Running in a different thread + // And there is no Executor interface with these requirements + // These factories clean up the pools on finalizer. + // Package private so the FakeHalFactory can dispatch + static class ExecutorHolder { + static final Executor CALLBACK_EXECUTOR = + Executors.newSingleThreadExecutor(); + static final Executor INJECTION_EXECUTOR = + Executors.newSingleThreadExecutor(); + } + + // Dispatcher interface for callbacks, using the executors above + private final InjectionDispatcher mInjectionDispatcher; + + // Created on construction, passed back to clients. + private final IInjectGlobalEvent.Stub mGlobalEventSession; + + @GuardedBy("mLock") + private IBinder.DeathRecipient mDeathRecipient; + + @GuardedBy("mLock") + private GlobalCallbackDispatcher mGlobalCallbackDispatcher = null; + + @GuardedBy("mLock") + private boolean mIsResourceContended = false; + @GuardedBy("mLock") + private final Map<Integer, ModelSession> mModelSessionMap = new HashMap<>(); + + // Current version of the STHAL relies on integer model session ids. + // Generate them monotonically starting at 101 + @GuardedBy("mLock") + private int mModelKeyCounter = 101; + + @GuardedBy("mLock") + private boolean mIsDead = false; + + private class ModelSession extends IInjectModelEvent.Stub { + // Logically const + private final boolean mIsKeyphrase; + private final CallbackDispatcher mCallbackDispatcher; + private final int mModelHandle; + + // Model parameter + @GuardedBy("FakeSoundTriggerHal.this.mLock") + private int mThreshold = 0; + + // Mutable + @GuardedBy("FakeSoundTriggerHal.this.mLock") + private boolean mIsUnloaded = false; // Latch + + // Only a single recognition session is able to be active for a model + // session at any given time. Null if no recognition is active. + @GuardedBy("FakeSoundTriggerHal.this.mLock") + @Nullable private RecognitionSession mRecognitionSession; + + private ModelSession(int modelHandle, CallbackDispatcher callbackDispatcher, + boolean isKeyphrase) { + mModelHandle = modelHandle; + mCallbackDispatcher = callbackDispatcher; + mIsKeyphrase = isKeyphrase; + } + + private RecognitionSession startRecognitionForModel() { + synchronized (FakeSoundTriggerHal.this.mLock) { + mRecognitionSession = new RecognitionSession(); + return mRecognitionSession; + } + } + + private RecognitionSession stopRecognitionForModel() { + synchronized (FakeSoundTriggerHal.this.mLock) { + RecognitionSession session = mRecognitionSession; + mRecognitionSession = null; + return session; + } + } + + private void forceRecognitionForModel() { + synchronized (FakeSoundTriggerHal.this.mLock) { + if (mIsKeyphrase) { + PhraseRecognitionEvent phraseEvent = + createDefaultKeyphraseEvent(RecognitionStatus.FORCED); + mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> + cb.phraseRecognitionCallback(mModelHandle, phraseEvent)); + } else { + RecognitionEvent event = createDefaultEvent(RecognitionStatus.FORCED); + mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> + cb.recognitionCallback(mModelHandle, event)); + } + } + } + + private void setThresholdFactor(int value) { + synchronized (FakeSoundTriggerHal.this.mLock) { + mThreshold = value; + } + } + + private int getThresholdFactor() { + synchronized (FakeSoundTriggerHal.this.mLock) { + return mThreshold; + } + } + + private boolean getIsUnloaded() { + synchronized (FakeSoundTriggerHal.this.mLock) { + return mIsUnloaded; + } + } + + private RecognitionSession getRecogSession() { + synchronized (FakeSoundTriggerHal.this.mLock) { + return mRecognitionSession; + } + } + + + /** oneway **/ + @Override + public void triggerUnloadModel() { + synchronized (FakeSoundTriggerHal.this.mLock) { + if (mIsDead || mIsUnloaded) return; + if (mRecognitionSession != null) { + // Must abort model before triggering unload + mRecognitionSession.triggerAbortRecognition(); + } + // Invalidate the model session + mIsUnloaded = true; + mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> + cb.modelUnloaded(mModelHandle)); + // Don't notify the injection that an unload has occurred, since it is what + // triggered the unload + + // Notify if we could have denied a previous model due to contention + if (getNumLoadedModelsLocked() == (mProperties.maxSoundModels - 1) + && !mIsResourceContended) { + mGlobalCallbackDispatcher.wrap((ISoundTriggerHwGlobalCallback cb) -> + cb.onResourcesAvailable()); + } + } + } + + private class RecognitionSession extends IInjectRecognitionEvent.Stub { + + @Override + /** oneway **/ + public void triggerRecognitionEvent(byte[] data, + @Nullable PhraseRecognitionExtra[] phraseExtras) { + synchronized (FakeSoundTriggerHal.this.mLock) { + // Check if our session has already been invalidated + if (mIsDead || mRecognitionSession != this) return; + // Invalidate the recognition session + mRecognitionSession = null; + // Trigger the callback. + if (mIsKeyphrase) { + PhraseRecognitionEvent phraseEvent = + createDefaultKeyphraseEvent(RecognitionStatus.SUCCESS); + phraseEvent.common.data = data; + if (phraseExtras != null) phraseEvent.phraseExtras = phraseExtras; + mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> + cb.phraseRecognitionCallback(mModelHandle, phraseEvent)); + } else { + RecognitionEvent event = createDefaultEvent(RecognitionStatus.SUCCESS); + event.data = data; + mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> + cb.recognitionCallback(mModelHandle, event)); + } + } + } + + @Override + /** oneway **/ + public void triggerAbortRecognition() { + synchronized (FakeSoundTriggerHal.this.mLock) { + if (mIsDead || mRecognitionSession != this) return; + // Clear the session state + mRecognitionSession = null; + // Trigger the callback. + if (mIsKeyphrase) { + mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> + cb.phraseRecognitionCallback(mModelHandle, + createDefaultKeyphraseEvent(RecognitionStatus.ABORTED))); + } else { + mCallbackDispatcher.wrap((ISoundTriggerHwCallback cb) -> + cb.recognitionCallback(mModelHandle, + createDefaultEvent(RecognitionStatus.ABORTED))); + } + } + } + } + } + + // Since this is always constructed, it needs to be cheap to create. + public FakeSoundTriggerHal(ISoundTriggerInjection injection) { + mProperties = createDefaultProperties(); + mInjectionDispatcher = new InjectionDispatcher(injection); + mGlobalCallbackDispatcher = null; // If this NPEs before registration, we want to abort. + // Implement the IInjectGlobalEvent IInterface. + // Since we can't extend multiple IInterface from the same object, instantiate an instance + // for our clients. + mGlobalEventSession = new IInjectGlobalEvent.Stub() { + /** + * Overrides IInjectGlobalEvent method. + * Simulate a HAL process restart. This method is not included in regular HAL interface, + * since the entire process is restarted by sending a signal. + * Since we run in-proc, we must offer an explicit restart method. + * oneway + */ + @Override + public void triggerRestart() throws RemoteException { + synchronized (FakeSoundTriggerHal.this.mLock) { + if (mIsDead) throw new DeadObjectException(); + mIsDead = true; + mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> + cb.onRestarted(this)); + mModelSessionMap.clear(); + if (mDeathRecipient != null) { + final DeathRecipient deathRecipient = mDeathRecipient; + ExecutorHolder.CALLBACK_EXECUTOR.execute(() -> { + try { + deathRecipient.binderDied(FakeSoundTriggerHal.this.asBinder()); + } catch (Throwable e) { + // We don't expect RemoteException at the moment since we run + // in the same process + Slog.wtf(TAG, "Callback dispatch threw", e); + } + }); + } + } + } + + /** + * Overrides IInjectGlobalEvent method. + * oneway + */ + @Override + public void setResourceContention(boolean isResourcesContended, + IAcknowledgeEvent callback) throws RemoteException { + synchronized (FakeSoundTriggerHal.this.mLock) { + if (mIsDead) throw new DeadObjectException(); + mIsResourceContended = isResourcesContended; + // Introducing contention is the only injection which can't be + // observed by the ST client. + mInjectionDispatcher.wrap((ISoundTriggerInjection unused) -> + callback.eventReceived()); + if (!mIsResourceContended) { + mGlobalCallbackDispatcher.wrap((ISoundTriggerHwGlobalCallback cb) -> + cb.onResourcesAvailable()); + } + } + } + }; + // Register the global event injection interface + mInjectionDispatcher.wrap((ISoundTriggerInjection cb) + -> cb.registerGlobalEventInjection(mGlobalEventSession)); + } + + /** + * Get the {@link IInjectGlobalEvent} associated with this instance of the STHAL. + * Used as a session token, valid until restarted. + */ + public IInjectGlobalEvent getGlobalEventInjection() { + return mGlobalEventSession; + } + + // TODO(b/274467228) we can remove the next three methods when this HAL is moved out-of-proc, + // so process restart at death notification is appropriately handled by the binder. + @Override + public void linkToDeath(IBinder.DeathRecipient recipient, int flags) { + synchronized (mLock) { + if (mDeathRecipient != null) { + Slog.wtf(TAG, "Received two death recipients concurrently"); + } + mDeathRecipient = recipient; + } + } + + @Override + public boolean unlinkToDeath(IBinder.DeathRecipient recipient, int flags) { + synchronized (mLock) { + if (mIsDead) return false; + if (mDeathRecipient != recipient) { + throw new NoSuchElementException(); + } + mDeathRecipient = null; + return true; + } + } + + // STHAL method overrides to follow + @Override + public Properties getProperties() throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + Parcel parcel = Parcel.obtain(); + try { + mProperties.writeToParcel(parcel, 0 /* flags */); + parcel.setDataPosition(0); + return Properties.CREATOR.createFromParcel(parcel); + } finally { + parcel.recycle(); + } + } + } + + @Override + public void registerGlobalCallback( + ISoundTriggerHwGlobalCallback callback) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + mGlobalCallbackDispatcher = new GlobalCallbackDispatcher(callback); + } + } + + @Override + public int loadSoundModel(SoundModel soundModel, + ISoundTriggerHwCallback callback) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + if (mIsResourceContended || getNumLoadedModelsLocked() == mProperties.maxSoundModels) { + throw new ServiceSpecificException(Status.RESOURCE_CONTENTION); + } + int key = mModelKeyCounter++; + ModelSession session = new ModelSession(key, new CallbackDispatcher(callback), false); + + mModelSessionMap.put(key, session); + + mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> + cb.onSoundModelLoaded(soundModel, null, session, mGlobalEventSession)); + return key; + } + } + + @Override + public int loadPhraseSoundModel(PhraseSoundModel soundModel, + ISoundTriggerHwCallback callback) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + if (mIsResourceContended || getNumLoadedModelsLocked() == mProperties.maxSoundModels) { + throw new ServiceSpecificException(Status.RESOURCE_CONTENTION); + } + + int key = mModelKeyCounter++; + ModelSession session = new ModelSession(key, new CallbackDispatcher(callback), true); + + mModelSessionMap.put(key, session); + + mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> + cb.onSoundModelLoaded(soundModel.common, soundModel.phrases, session, + mGlobalEventSession)); + return key; + } + } + + @Override + public void unloadSoundModel(int modelHandle) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + ModelSession session = mModelSessionMap.get(modelHandle); + if (session == null) { + Slog.wtf(TAG, "Attempted to unload model which was never loaded"); + } + + if (session.getRecogSession() != null) { + Slog.wtf(TAG, "Session unloaded before recog stopped!"); + } + + // Session is stale + if (session.getIsUnloaded()) return; + mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> + cb.onSoundModelUnloaded(session)); + + // Notify if we could have denied a previous model due to contention + if (getNumLoadedModelsLocked() == (mProperties.maxSoundModels - 1) + && !mIsResourceContended) { + mGlobalCallbackDispatcher.wrap((ISoundTriggerHwGlobalCallback cb) -> + cb.onResourcesAvailable()); + } + + } + } + + @Override + public void startRecognition(int modelHandle, int deviceHandle, int ioHandle, + RecognitionConfig config) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + ModelSession session = mModelSessionMap.get(modelHandle); + if (session == null) { + Slog.wtf(TAG, "Attempted to start recognition with invalid handle"); + } + + if (session.getIsUnloaded()) { + // TODO(b/274470274) this is a deficiency in the existing HAL API, there is no way + // to handle this race gracefully + throw new ServiceSpecificException(Status.RESOURCE_CONTENTION); + } + ModelSession.RecognitionSession recogSession = session.startRecognitionForModel(); + + // TODO(b/274470571) appropriately translate ioHandle to session handle + mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> + cb.onRecognitionStarted(-1, config, recogSession, session)); + } + } + + @Override + public void stopRecognition(int modelHandle) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + ModelSession session = mModelSessionMap.get(modelHandle); + if (session == null) { + Slog.wtf(TAG, "Attempted to stop recognition with invalid handle"); + } + ModelSession.RecognitionSession recogSession = session.stopRecognitionForModel(); + mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> + cb.onRecognitionStopped(recogSession)); + } + } + + @Override + public void forceRecognitionEvent(int modelHandle) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + ModelSession session = mModelSessionMap.get(modelHandle); + if (session == null) { + Slog.wtf(TAG, "Attempted to force recognition with invalid handle"); + } + + // TODO(b/274470274) this is a deficiency in the existing HAL API, we could always + // get a force request for an already stopped model. The only thing to do is + // drop such a request. + if (session.getRecogSession() == null) return; + session.forceRecognitionForModel(); + } + } + + // TODO(b/274470274) this is a deficiency in the existing HAL API, we could always + // get model param API requests after model unload. + // For now, succeed anyway to maintain fidelity to existing HALs. + @Override + public @Nullable ModelParameterRange queryParameter(int modelHandle, + /** ModelParameter **/ int modelParam) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + ModelSession session = mModelSessionMap.get(modelHandle); + if (session == null) { + Slog.wtf(TAG, "Attempted to get param with invalid handle"); + } + } + if (modelParam == ModelParameter.THRESHOLD_FACTOR) { + ModelParameterRange range = new ModelParameterRange(); + range.minInclusive = THRESHOLD_MIN; + range.maxInclusive = THRESHOLD_MAX; + return range; + } else { + return null; + } + } + + @Override + public int getParameter(int modelHandle, + /** ModelParameter **/ int modelParam) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + ModelSession session = mModelSessionMap.get(modelHandle); + if (session == null) { + Slog.wtf(TAG, "Attempted to get param with invalid handle"); + } + if (modelParam != ModelParameter.THRESHOLD_FACTOR) { + throw new IllegalArgumentException(); + } + return session.getThresholdFactor(); + } + } + + @Override + public void setParameter(int modelHandle, + /** ModelParameter **/ int modelParam, int value) throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + ModelSession session = mModelSessionMap.get(modelHandle); + if (session == null) { + Slog.wtf(TAG, "Attempted to get param with invalid handle"); + } + if ((modelParam == ModelParameter.THRESHOLD_FACTOR) + || (value >= THRESHOLD_MIN && value <= THRESHOLD_MAX)) { + session.setThresholdFactor(value); + } else { + throw new IllegalArgumentException(); + } + mInjectionDispatcher.wrap((ISoundTriggerInjection cb) -> + cb.onParamSet(modelParam, value, session)); + } + } + + @Override + public int getInterfaceVersion() throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + } + return super.VERSION; + } + + @Override + public String getInterfaceHash() throws RemoteException { + synchronized (mLock) { + if (mIsDead) throw new DeadObjectException(); + } + return super.HASH; + } + + // Helpers to follow. + @GuardedBy("mLock") + private int getNumLoadedModelsLocked() { + int numModels = 0; + for (ModelSession session : mModelSessionMap.values()) { + if (!session.getIsUnloaded()) { + numModels++; + } + } + return numModels; + } + + private static Properties createDefaultProperties() { + Properties properties = new Properties(); + properties.implementor = "android"; + properties.description = "AOSP fake STHAL"; + properties.version = 1; + properties.uuid = "00000001-0002-0003-0004-deadbeefabcd"; + properties.supportedModelArch = ISoundTriggerInjection.FAKE_HAL_ARCH; + properties.maxSoundModels = 8; + properties.maxKeyPhrases = 2; + properties.maxUsers = 2; + properties.recognitionModes = RecognitionMode.VOICE_TRIGGER + | RecognitionMode.GENERIC_TRIGGER; + properties.captureTransition = true; + // This is actually not respected, since there is no real AudioRecord + properties.maxBufferMs = 5000; + properties.concurrentCapture = true; + properties.triggerInEvent = false; + properties.powerConsumptionMw = 0; + properties.audioCapabilities = 0; + return properties; + } + + private static RecognitionEvent createDefaultEvent( + /** RecognitionStatus **/ int status) { + RecognitionEvent event = new RecognitionEvent(); + // Overwrite the event appropriately. + event.status = status; + event.type = SoundModelType.GENERIC; + // TODO(b/274466981) make this configurable. + // For now, some plausible defaults + event.captureAvailable = true; + event.captureDelayMs = 50; + event.capturePreambleMs = 200; + event.triggerInData = false; + event.audioConfig = null; // Nullable within AIDL + event.data = new byte[0]; + // We don't support recognition restart for now + event.recognitionStillActive = false; + return event; + } + + private static PhraseRecognitionEvent createDefaultKeyphraseEvent( + /**RecognitionStatus **/ int status) { + RecognitionEvent event = createDefaultEvent(status); + event.type = SoundModelType.KEYPHRASE; + PhraseRecognitionEvent phraseEvent = new PhraseRecognitionEvent(); + phraseEvent.common = event; + phraseEvent.phraseExtras = new PhraseRecognitionExtra[0]; + return phraseEvent; + } + + // Helper classes to dispatch oneway calls to the appropriate callback interfaces to follow. + private static class CallbackDispatcher { + + private CallbackDispatcher(ISoundTriggerHwCallback callback) { + mCallback = callback; + } + + private void wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerHwCallback> command) { + ExecutorHolder.CALLBACK_EXECUTOR.execute(() -> { + try { + command.accept(mCallback); + } catch (Throwable e) { + Slog.wtf(TAG, "Callback dispatch threw", e); + } + }); + } + + private final ISoundTriggerHwCallback mCallback; + } + + private static class GlobalCallbackDispatcher { + + private GlobalCallbackDispatcher(ISoundTriggerHwGlobalCallback callback) { + mCallback = callback; + } + + private void wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerHwGlobalCallback> command) { + ExecutorHolder.CALLBACK_EXECUTOR.execute(() -> { + try { + command.accept(mCallback); + } catch (Throwable e) { + // We don't expect RemoteException at the moment since we run + // in the same process + Slog.wtf(TAG, "Callback dispatch threw", e); + } + }); + } + + private final ISoundTriggerHwGlobalCallback mCallback; + } + + private static class InjectionDispatcher { + + private InjectionDispatcher(ISoundTriggerInjection injection) { + mInjection = injection; + } + + private void wrap(FunctionalUtils.ThrowingConsumer<ISoundTriggerInjection> command) { + ExecutorHolder.INJECTION_EXECUTOR.execute(() -> { + try { + command.accept(mInjection); + } catch (Throwable e) { + // We don't expect RemoteException at the moment since we run + // in the same process + Slog.wtf(TAG, "Callback dispatch threw", e); + } + }); + } + + private final ISoundTriggerInjection mInjection; + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ISoundTriggerHal.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ISoundTriggerHal.java index aa85dd01405e..75206e69bc6a 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ISoundTriggerHal.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ISoundTriggerHal.java @@ -141,6 +141,22 @@ interface ISoundTriggerHal { void flushCallbacks(); /** + * Used only for testing purposes. Called when a client attaches to the framework. + * Transmitting this event to the fake STHAL allows observation of this event, which is + * normally consumed by the framework, and is not communicated to the STHAL. + * @param token - A unique binder token associated with this session. + */ + void clientAttached(IBinder token); + + /** + * Used only for testing purposes. Called when a client detached from the framework. + * Transmitting this event to the fake STHAL allows observation of this event, which is + * normally consumed by the framework, and is not communicated to the STHAL. + * @param token - The same token passed to the corresponding {@link clientAttached(IBinder)}. + */ + void clientDetached(IBinder token); + + /** * Kill and restart the HAL instance. This is typically a last resort for error recovery and may * result in other related services being killed. */ diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java index b0f03ef48e7e..8c7cabeee320 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java @@ -283,6 +283,16 @@ public class SoundTriggerHalConcurrentCaptureHandler implements ISoundTriggerHal mCallbackThread.flush(); } + @Override + public void clientAttached(IBinder binder) { + mDelegate.clientAttached(binder); + } + + @Override + public void clientDetached(IBinder binder) { + mDelegate.clientDetached(binder); + } + /** * This is a thread for asynchronous delivery of callback events, having the following features: * <ul> diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java index 235d10fb5da3..24741e1caea9 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java @@ -211,6 +211,16 @@ public class SoundTriggerHalEnforcer implements ISoundTriggerHal { mUnderlying.flushCallbacks(); } + @Override + public void clientAttached(IBinder binder) { + mUnderlying.clientAttached(binder); + } + + @Override + public void clientDetached(IBinder binder) { + mUnderlying.clientDetached(binder); + } + private RuntimeException handleException(RuntimeException e) { if (e instanceof RecoverableException) { throw e; diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalMaxModelLimiter.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalMaxModelLimiter.java index 7dd28e0b88f9..8cdd2697db50 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalMaxModelLimiter.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalMaxModelLimiter.java @@ -165,4 +165,14 @@ public class SoundTriggerHalMaxModelLimiter implements ISoundTriggerHal { public void flushCallbacks() { mDelegate.flushCallbacks(); } + + @Override + public void clientAttached(IBinder binder) { + mDelegate.clientAttached(binder); + } + + @Override + public void clientDetached(IBinder binder) { + mDelegate.clientDetached(binder); + } } diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java index b817821b48dc..0390f034ab23 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java @@ -143,6 +143,16 @@ public class SoundTriggerHalWatchdog implements ISoundTriggerHal { } @Override + public void clientAttached(IBinder binder) { + mUnderlying.clientAttached(binder); + } + + @Override + public void clientDetached(IBinder binder) { + mUnderlying.clientDetached(binder); + } + + @Override public void reboot() { mUnderlying.reboot(); } diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java index 9bbae4bd2db5..c67bdd76eee8 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java @@ -414,6 +414,16 @@ final class SoundTriggerHw2Compat implements ISoundTriggerHal { // This is a no-op. Only implemented for decorators. } + @Override + public void clientAttached(IBinder binder) { + // This is a no-op. Only implemented for decorators. + } + + @Override + public void clientDetached(IBinder binder) { + // This is a no-op. Only implemented for decorators. + } + private Properties getProperties_2_0() throws RemoteException { AtomicInteger retval = new AtomicInteger(-1); diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw3Compat.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw3Compat.java index ebe0ff85167d..8bb5eb191858 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw3Compat.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw3Compat.java @@ -186,6 +186,16 @@ public class SoundTriggerHw3Compat implements ISoundTriggerHal { } @Override + public void clientAttached(IBinder binder) { + // No-op. This method is for test purposes, and is intercepted above. + } + + @Override + public void clientDetached(IBinder binder) { + // No-op. This method is for test purposes, and is intercepted above. + } + + @Override public void reboot() { mRebootRunnable.run(); } diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerInjection.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerInjection.java new file mode 100644 index 000000000000..30e079475b7c --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerInjection.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.soundtrigger_middleware; + +import android.annotation.Nullable; +import android.media.soundtrigger.Phrase; +import android.media.soundtrigger.RecognitionConfig; +import android.media.soundtrigger.SoundModel; +import android.media.soundtrigger_middleware.IInjectGlobalEvent; +import android.media.soundtrigger_middleware.IInjectModelEvent; +import android.media.soundtrigger_middleware.IInjectRecognitionEvent; +import android.media.soundtrigger_middleware.ISoundTriggerInjection; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Slog; + +import com.android.internal.annotations.GuardedBy; + +import java.util.Objects; + +/** + * Service side of the injection interface which enforces a single client. + * Essentially a facade that presents an ever-present, single injection client to the fake STHAL. + * Proxies a binder interface, but should never be called as such. + * @hide + */ + +public class SoundTriggerInjection implements ISoundTriggerInjection, IBinder.DeathRecipient { + + private static final String TAG = "SoundTriggerInjection"; + + private final Object mClientLock = new Object(); + @GuardedBy("mClientLock") + private ISoundTriggerInjection mClient = null; + @GuardedBy("mClientLock") + private IInjectGlobalEvent mGlobalEventInjection = null; + + /** + * Register a remote injection client. + * @param client - The injection client to register + */ + public void registerClient(ISoundTriggerInjection client) { + synchronized (mClientLock) { + Objects.requireNonNull(client); + if (mClient != null) { + try { + mClient.onPreempted(); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException when handling preemption", e); + } + mClient.asBinder().unlinkToDeath(this, 0); + } + mClient = client; + // Register cached global event injection interfaces, + // in case our client missed them. + try { + mClient.asBinder().linkToDeath(this, 0); + if (mGlobalEventInjection != null) { + mClient.registerGlobalEventInjection(mGlobalEventInjection); + } + } catch (RemoteException e) { + mClient = null; + } + + } + } + + @Override + public void binderDied() { + Slog.wtf(TAG, "Binder died without params"); + } + + // If the binder has died, clear out mClient. + @Override + public void binderDied(IBinder who) { + synchronized (mClientLock) { + if (mClient != null && who == mClient.asBinder()) { + mClient = null; + } + } + } + + @Override + public void registerGlobalEventInjection(IInjectGlobalEvent globalInjection) { + synchronized (mClientLock) { + // Cache for late attaching clients + mGlobalEventInjection = globalInjection; + if (mClient == null) return; + try { + mClient.registerGlobalEventInjection(mGlobalEventInjection); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onRestarted(IInjectGlobalEvent globalSession) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onRestarted(globalSession); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onFrameworkDetached(IInjectGlobalEvent globalSession) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onFrameworkDetached(globalSession); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onClientAttached(IBinder token, IInjectGlobalEvent globalSession) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onClientAttached(token, globalSession); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onClientDetached(IBinder token) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onClientDetached(token); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onSoundModelLoaded(SoundModel model, @Nullable Phrase[] phrases, + IInjectModelEvent modelInjection, IInjectGlobalEvent globalSession) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onSoundModelLoaded(model, phrases, modelInjection, globalSession); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onParamSet(/** ModelParameter **/ int modelParam, int value, + IInjectModelEvent modelSession) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onParamSet(modelParam, value, modelSession); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onRecognitionStarted(int audioSessionToken, RecognitionConfig config, + IInjectRecognitionEvent recognitionInjection, IInjectModelEvent modelSession) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onRecognitionStarted(audioSessionToken, config, + recognitionInjection, modelSession); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onRecognitionStopped(IInjectRecognitionEvent recognitionSession) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onRecognitionStopped(recognitionSession); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onSoundModelUnloaded(IInjectModelEvent modelSession) { + synchronized (mClientLock) { + if (mClient == null) return; + try { + mClient.onSoundModelUnloaded(modelSession); + } catch (RemoteException e) { + mClient = null; + } + } + } + + @Override + public void onPreempted() { + // We are the service, so we can't be preempted. + Slog.wtf(TAG, "Unexpected preempted!"); + } + + @Override + public IBinder asBinder() { + // This class is not a real binder object + Slog.wtf(TAG, "Unexpected asBinder!"); + throw new UnsupportedOperationException("Calling asBinder on a fake binder object"); + } + +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java index 807ed14e85ce..91e546696971 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java @@ -20,6 +20,7 @@ import static android.Manifest.permission.SOUNDTRIGGER_DELEGATE_IDENTITY; import android.annotation.NonNull; import android.content.Context; +import android.content.PermissionChecker; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.Identity; import android.media.permission.PermissionUtil; @@ -29,6 +30,7 @@ import android.media.soundtrigger.PhraseSoundModel; import android.media.soundtrigger.RecognitionConfig; import android.media.soundtrigger.SoundModel; import android.media.soundtrigger_middleware.ISoundTriggerCallback; +import android.media.soundtrigger_middleware.ISoundTriggerInjection; import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService; import android.media.soundtrigger_middleware.ISoundTriggerModule; import android.media.soundtrigger_middleware.SoundTriggerModuleDescriptor; @@ -68,15 +70,18 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic private final @NonNull ISoundTriggerMiddlewareInternal mDelegate; private final @NonNull Context mContext; + // Lightweight object used to delegate injection events to the fake STHAL + private final @NonNull SoundTriggerInjection mInjection; /** * Constructor for internal use only. Could be exposed for testing purposes in the future. * Users should access this class via {@link Lifecycle}. */ private SoundTriggerMiddlewareService(@NonNull ISoundTriggerMiddlewareInternal delegate, - @NonNull Context context) { + @NonNull Context context, @NonNull SoundTriggerInjection injection) { mDelegate = Objects.requireNonNull(delegate); mContext = context; + mInjection = injection; } @Override @@ -114,6 +119,16 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic } @Override + @android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER) + public void attachFakeHalInjection(@NonNull ISoundTriggerInjection injection) { + PermissionChecker.checkCallingOrSelfPermissionForPreflight( + mContext, android.Manifest.permission.MANAGE_SOUND_TRIGGER); + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + mInjection.registerClient(Objects.requireNonNull(injection)); + } + } + + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { if (mDelegate instanceof Dumpable) { ((Dumpable) mDelegate).dump(fout); @@ -223,7 +238,9 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic @Override public void onStart() { - HalFactory[] factories = new HalFactory[]{new DefaultHalFactory()}; + final SoundTriggerInjection injection = new SoundTriggerInjection(); + HalFactory[] factories = new HalFactory[]{new DefaultHalFactory(), + new FakeHalFactory(injection)}; publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE, new SoundTriggerMiddlewareService( @@ -232,7 +249,8 @@ public class SoundTriggerMiddlewareService extends ISoundTriggerMiddlewareServic new SoundTriggerMiddlewareValidation( new SoundTriggerMiddlewareImpl(factories, new AudioSessionProviderImpl())), - getContext())), getContext())); + getContext())), getContext(), + injection)); } } } diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java index fd8dee8416f6..d2d8f1ad7a71 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java @@ -29,6 +29,7 @@ import android.media.soundtrigger.SoundModelType; import android.media.soundtrigger.Status; import android.media.soundtrigger_middleware.ISoundTriggerCallback; import android.media.soundtrigger_middleware.ISoundTriggerModule; +import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.util.Log; @@ -38,6 +39,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -92,13 +94,14 @@ class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.Glo /** * Ctor. * - * @param halFactory A factory for the underlying HAL driver. + * @param halFactory - A factory for the underlying HAL driver. + * @param audioSessionProvider - Creates a session token + device id/port pair used to + * associate recognition events with the audio stream used to access data. */ SoundTriggerModule(@NonNull HalFactory halFactory, @NonNull SoundTriggerMiddlewareImpl.AudioSessionProvider audioSessionProvider) { - assert halFactory != null; - mHalFactory = halFactory; - mAudioSessionProvider = audioSessionProvider; + mHalFactory = Objects.requireNonNull(halFactory); + mAudioSessionProvider = Objects.requireNonNull(audioSessionProvider); attachToHal(); } @@ -218,6 +221,7 @@ class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.Glo */ private class Session implements ISoundTriggerModule { private ISoundTriggerCallback mCallback; + private final IBinder mToken = new Binder(); private final Map<Integer, Model> mLoadedModels = new HashMap<>(); /** @@ -227,6 +231,7 @@ class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.Glo */ private Session(@NonNull ISoundTriggerCallback callback) { mCallback = callback; + mHalService.clientAttached(mToken); } @Override @@ -237,6 +242,7 @@ class SoundTriggerModule implements IBinder.DeathRecipient, ISoundTriggerHal.Glo } removeSession(this); mCallback = null; + mHalService.clientDetached(mToken); } } |