Merge "Implement fake STHAL" into udc-dev

This commit is contained in:
Atneya Nair
2023-03-27 03:32:05 +00:00
committed by Android (Google) Code Review
13 changed files with 1162 additions and 8 deletions

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -140,6 +140,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.

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -142,6 +142,16 @@ public class SoundTriggerHalWatchdog implements ISoundTriggerHal {
mUnderlying.flushCallbacks();
}
@Override
public void clientAttached(IBinder binder) {
mUnderlying.clientAttached(binder);
}
@Override
public void clientDetached(IBinder binder) {
mUnderlying.clientDetached(binder);
}
@Override
public void reboot() {
mUnderlying.reboot();

View File

@@ -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);

View File

@@ -185,6 +185,16 @@ public class SoundTriggerHw3Compat implements ISoundTriggerHal {
// No-op.
}
@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();

View File

@@ -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");
}
}

View File

@@ -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
@@ -113,6 +118,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) {
@@ -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));
}
}
}

View File

@@ -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);
}
}