Merge "Implement fake STHAL" into udc-dev
This commit is contained in:
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user