summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Atneya Nair <atneya@google.com> 2023-02-28 18:47:16 -0800
committer Atneya Nair <atneya@google.com> 2023-03-26 18:31:03 -0700
commit890b2b360129e3a6d47b945b14e64e2635433573 (patch)
tree685adbd89fcdef246ee50346ae94147297a18491
parent290172443ab77336af0b49df4e84dbd6cc76f92f (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
-rw-r--r--media/aidl/android/media/soundtrigger_middleware/ISoundTriggerMiddlewareService.aidl12
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/FakeHalFactory.java96
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/FakeSoundTriggerHal.java712
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/ISoundTriggerHal.java16
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalConcurrentCaptureHandler.java10
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalEnforcer.java10
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalMaxModelLimiter.java10
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHalWatchdog.java10
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw2Compat.java10
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerHw3Compat.java10
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerInjection.java236
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java24
-rw-r--r--services/voiceinteraction/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java14
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);
}
}