678 lines
28 KiB
Java
678 lines
28 KiB
Java
![]() |
/**
|
||
|
* 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 android.media.soundtrigger;
|
||
|
|
||
|
import android.annotation.CallbackExecutor;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.annotation.RequiresPermission;
|
||
|
import android.annotation.TestApi;
|
||
|
import android.hardware.soundtrigger.ConversionUtil;
|
||
|
import android.hardware.soundtrigger.SoundTrigger;
|
||
|
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.IBinder;
|
||
|
import android.os.RemoteException;
|
||
|
|
||
|
import com.android.internal.annotations.GuardedBy;
|
||
|
import com.android.internal.app.ISoundTriggerService;
|
||
|
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Arrays;
|
||
|
import java.util.HashMap;
|
||
|
import java.util.List;
|
||
|
import java.util.Map;
|
||
|
import java.util.Objects;
|
||
|
import java.util.UUID;
|
||
|
import java.util.concurrent.CountDownLatch;
|
||
|
import java.util.concurrent.Executor;
|
||
|
import java.util.function.Consumer;
|
||
|
|
||
|
/**
|
||
|
* Used to inject/observe events when using a fake SoundTrigger HAL for test purposes.
|
||
|
* Created by {@link SoundTriggerManager#getInjection(Executor, GlobalCallback)}.
|
||
|
* Only one instance of this class is valid at any given time, old instances will be delivered
|
||
|
* {@link GlobalCallback#onPreempted()}.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public final class SoundTriggerInstrumentation {
|
||
|
|
||
|
private final Object mLock = new Object();
|
||
|
@GuardedBy("mLock")
|
||
|
private IInjectGlobalEvent mInjectGlobalEvent = null;
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private Map<IBinder, ModelSession> mModelSessionMap = new HashMap<>();
|
||
|
@GuardedBy("mLock")
|
||
|
private Map<IBinder, RecognitionSession> mRecognitionSessionMap = new HashMap<>();
|
||
|
@GuardedBy("mLock")
|
||
|
private IBinder mClientToken = null;
|
||
|
|
||
|
private final ISoundTriggerService mService;
|
||
|
|
||
|
private final GlobalCallback mClientCallback;
|
||
|
private final Executor mGlobalCallbackExecutor;
|
||
|
|
||
|
/**
|
||
|
* Callback interface for un-sessioned events observed from the fake STHAL.
|
||
|
* Registered upon construction of {@link SoundTriggerInstrumentation}
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public interface GlobalCallback {
|
||
|
/**
|
||
|
* Called when the created {@link SoundTriggerInstrumentation} object is invalidated
|
||
|
* by another client creating an {@link SoundTriggerInstrumentation} to instrument the
|
||
|
* fake STHAL. Only one client may inject at a time.
|
||
|
* All sessions are invalidated, no further events will be received, and no
|
||
|
* injected events will be delivered.
|
||
|
*/
|
||
|
default void onPreempted() {}
|
||
|
/**
|
||
|
* Called when the STHAL has been restarted by the framework, due to unexpected
|
||
|
* error conditions.
|
||
|
* Not called when {@link SoundTriggerInstrumentation#triggerRestart()} is injected.
|
||
|
*/
|
||
|
default void onRestarted() {}
|
||
|
/**
|
||
|
* Called when the framework detaches from the fake HAL.
|
||
|
* This is not transmitted to real HALs, but it indicates that the
|
||
|
* framework has flushed its global state.
|
||
|
*/
|
||
|
default void onFrameworkDetached() {}
|
||
|
/**
|
||
|
* Called when a client application attaches to the framework.
|
||
|
* This is not transmitted to real HALs, but it represents the state of
|
||
|
* the framework.
|
||
|
*/
|
||
|
default void onClientAttached() {}
|
||
|
/**
|
||
|
* Called when a client application detaches from the framework.
|
||
|
* This is not transmitted to real HALs, but it represents the state of
|
||
|
* the framework.
|
||
|
*/
|
||
|
default void onClientDetached() {}
|
||
|
/**
|
||
|
* Called when the fake HAL receives a model load from the framework.
|
||
|
* @param modelSession - A session which exposes additional injection
|
||
|
* functionality associated with the newly loaded
|
||
|
* model. See {@link ModelSession}.
|
||
|
*/
|
||
|
void onModelLoaded(@NonNull ModelSession modelSession);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callback for HAL events related to a loaded model. Register with
|
||
|
* {@link ModelSession#setModelCallback(Executor, ModelCallback)}
|
||
|
* Note, callbacks will not be delivered for events triggered by the injection.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public interface ModelCallback {
|
||
|
/**
|
||
|
* Called when the model associated with the {@link ModelSession} this callback
|
||
|
* was registered for was unloaded by the framework.
|
||
|
*/
|
||
|
default void onModelUnloaded() {}
|
||
|
/**
|
||
|
* Called when the model associated with the {@link ModelSession} this callback
|
||
|
* was registered for receives a set parameter call from the framework.
|
||
|
* @param param - Parameter being set.
|
||
|
* See {@link SoundTrigger.ModelParamTypes}
|
||
|
* @param value - Value the model parameter was set to.
|
||
|
*/
|
||
|
default void onParamSet(@SoundTrigger.ModelParamTypes int param, int value) {}
|
||
|
/**
|
||
|
* Called when the model associated with the {@link ModelSession} this callback
|
||
|
* was registered for receives a recognition start request.
|
||
|
* @param recognitionSession - A session which exposes additional injection
|
||
|
* functionality associated with the newly started
|
||
|
* recognition. See {@link RecognitionSession}
|
||
|
*/
|
||
|
void onRecognitionStarted(@NonNull RecognitionSession recognitionSession);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Callback for HAL events related to a started recognition. Register with
|
||
|
* {@link RecognitionSession#setRecognitionCallback(Executor, RecognitionCallback)}
|
||
|
* Note, callbacks will not be delivered for events triggered by the injection.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public interface RecognitionCallback {
|
||
|
/**
|
||
|
* Called when the recognition associated with the {@link RecognitionSession} this
|
||
|
* callback was registered for was stopped by the framework.
|
||
|
*/
|
||
|
void onRecognitionStopped();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Session associated with a loaded model in the fake STHAL.
|
||
|
* Can be used to query details about the loaded model, register a callback for future
|
||
|
* model events, or trigger HAL events associated with a loaded model.
|
||
|
* This session is invalid once the model is unloaded, caused by a
|
||
|
* {@link ModelSession#triggerUnloadModel()},
|
||
|
* the client unloading recognition, or if a {@link GlobalCallback#onRestarted()} is
|
||
|
* received.
|
||
|
* Further injections on an invalidated session will not be respected, and no future
|
||
|
* callbacks will be delivered.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public class ModelSession {
|
||
|
|
||
|
/**
|
||
|
* Trigger the HAL to preemptively unload the model associated with this session.
|
||
|
* Typically occurs when a higher priority model is loaded which utilizes the same
|
||
|
* resources.
|
||
|
*/
|
||
|
public void triggerUnloadModel() {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
try {
|
||
|
mInjectModelEvent.triggerUnloadModel();
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
mModelSessionMap.remove(mInjectModelEvent.asBinder());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the {@link SoundTriggerManager.Model} associated with this session.
|
||
|
* @return - The model associated with this session.
|
||
|
*/
|
||
|
public @NonNull SoundTriggerManager.Model getSoundModel() {
|
||
|
return mModel;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the list of {@link SoundTrigger.Keyphrase} associated with this session.
|
||
|
* @return - The keyphrases associated with this session.
|
||
|
*/
|
||
|
public @NonNull List<SoundTrigger.Keyphrase> getPhrases() {
|
||
|
if (mPhrases == null) {
|
||
|
return new ArrayList<>();
|
||
|
} else {
|
||
|
return new ArrayList<>(Arrays.asList(mPhrases));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get whether this model is of keyphrase type.
|
||
|
* @return - true if the model is a keyphrase model, false otherwise
|
||
|
*/
|
||
|
public boolean isKeyphrase() {
|
||
|
return (mPhrases != null);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Registers the model callback associated with this session. Events associated
|
||
|
* with this model session will be reported via this callback.
|
||
|
* See {@link ModelCallback}
|
||
|
* @param executor - Executor which the callback is dispatched on
|
||
|
* @param callback - Model callback for reporting model session events.
|
||
|
*/
|
||
|
public void setModelCallback(@NonNull @CallbackExecutor Executor executor, @NonNull
|
||
|
ModelCallback callback) {
|
||
|
Objects.requireNonNull(callback);
|
||
|
Objects.requireNonNull(executor);
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (mModelCallback == null) {
|
||
|
for (var droppedConsumer : mDroppedConsumerList) {
|
||
|
executor.execute(() -> droppedConsumer.accept(callback));
|
||
|
}
|
||
|
mDroppedConsumerList.clear();
|
||
|
}
|
||
|
mModelCallback = callback;
|
||
|
mModelExecutor = executor;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clear the model callback associated with this session, if any has been
|
||
|
* set by {@link #setModelCallback(Executor, ModelCallback)}.
|
||
|
*/
|
||
|
public void clearModelCallback() {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
mModelCallback = null;
|
||
|
mModelExecutor = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private ModelSession(SoundModel model, Phrase[] phrases,
|
||
|
IInjectModelEvent injection) {
|
||
|
mModel = SoundTriggerManager.Model.create(UUID.fromString(model.uuid),
|
||
|
UUID.fromString(model.vendorUuid),
|
||
|
ConversionUtil.sharedMemoryToByteArray(model.data, model.dataSize));
|
||
|
if (phrases != null) {
|
||
|
mPhrases = new SoundTrigger.Keyphrase[phrases.length];
|
||
|
int i = 0;
|
||
|
for (var phrase : phrases) {
|
||
|
mPhrases[i++] = ConversionUtil.aidl2apiPhrase(phrase);
|
||
|
}
|
||
|
} else {
|
||
|
mPhrases = null;
|
||
|
}
|
||
|
mInjectModelEvent = injection;
|
||
|
}
|
||
|
|
||
|
private void wrap(Consumer<ModelCallback> consumer) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (mModelCallback != null) {
|
||
|
final ModelCallback callback = mModelCallback;
|
||
|
mModelExecutor.execute(() -> consumer.accept(callback));
|
||
|
} else {
|
||
|
mDroppedConsumerList.add(consumer);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private final SoundTriggerManager.Model mModel;
|
||
|
private final SoundTrigger.Keyphrase[] mPhrases;
|
||
|
private final IInjectModelEvent mInjectModelEvent;
|
||
|
|
||
|
@GuardedBy("SoundTriggerInstrumentation.this.mLock")
|
||
|
private ModelCallback mModelCallback = null;
|
||
|
@GuardedBy("SoundTriggerInstrumentation.this.mLock")
|
||
|
private Executor mModelExecutor = null;
|
||
|
@GuardedBy("SoundTriggerInstrumentation.this.mLock")
|
||
|
private final List<Consumer<ModelCallback>> mDroppedConsumerList = new ArrayList<>();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Session associated with a recognition start in the fake STHAL.
|
||
|
* Can be used to get information about the started recognition, register a callback
|
||
|
* for future events associated with this recognition, and triggering
|
||
|
* recognition events or aborts.
|
||
|
* This session is invalid once the recognition is stopped, caused by a
|
||
|
* {@link RecognitionSession#triggerAbortRecognition()},
|
||
|
* {@link RecognitionSession#triggerRecognitionEvent(byte[], List)},
|
||
|
* the client stopping recognition, or any operation which invalidates the
|
||
|
* {@link ModelSession} which the session was created from.
|
||
|
* Further injections on an invalidated session will not be respected, and no future
|
||
|
* callbacks will be delivered.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public class RecognitionSession {
|
||
|
|
||
|
/**
|
||
|
* Get an integer token representing the audio session associated with this
|
||
|
* recognition in the STHAL.
|
||
|
* @return - The session token.
|
||
|
*/
|
||
|
public int getAudioSession() {
|
||
|
return mAudioSession;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get the recognition config used to start this recognition.
|
||
|
* @return - The config passed to the HAL for startRecognition.
|
||
|
*/
|
||
|
public @NonNull SoundTrigger.RecognitionConfig getRecognitionConfig() {
|
||
|
return mRecognitionConfig;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Trigger a recognition in the fake STHAL.
|
||
|
* @param data - The opaque data buffer included in the recognition event.
|
||
|
* @param phraseExtras - Keyphrase metadata included in the event. The
|
||
|
* event must include metadata for the keyphrase id
|
||
|
* associated with this model to be received by the
|
||
|
* client application.
|
||
|
*/
|
||
|
public void triggerRecognitionEvent(@NonNull byte[] data, @Nullable
|
||
|
List<SoundTrigger.KeyphraseRecognitionExtra> phraseExtras) {
|
||
|
PhraseRecognitionExtra[] converted = null;
|
||
|
if (phraseExtras != null) {
|
||
|
converted = new PhraseRecognitionExtra[phraseExtras.size()];
|
||
|
int i = 0;
|
||
|
for (var phraseExtra : phraseExtras) {
|
||
|
converted[i++] = ConversionUtil.api2aidlPhraseRecognitionExtra(phraseExtra);
|
||
|
}
|
||
|
}
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
mRecognitionSessionMap.remove(mInjectRecognitionEvent.asBinder());
|
||
|
try {
|
||
|
mInjectRecognitionEvent.triggerRecognitionEvent(data, converted);
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Trigger an abort recognition event in the fake HAL. This represents a
|
||
|
* preemptive ending of the recognition session by the HAL, despite no
|
||
|
* recognition detection. Typically occurs during contention for microphone
|
||
|
* usage, or if model limits are hit.
|
||
|
* See {@link SoundTriggerInstrumentation#setResourceContention(boolean)} to block
|
||
|
* subsequent downward calls for contention reasons.
|
||
|
*/
|
||
|
public void triggerAbortRecognition() {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
mRecognitionSessionMap.remove(mInjectRecognitionEvent.asBinder());
|
||
|
try {
|
||
|
mInjectRecognitionEvent.triggerAbortRecognition();
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Registers the recognition callback associated with this session. Events associated
|
||
|
* with this recognition session will be reported via this callback.
|
||
|
* See {@link RecognitionCallback}
|
||
|
* @param executor - Executor which the callback is dispatched on
|
||
|
* @param callback - Recognition callback for reporting recognition session events.
|
||
|
*/
|
||
|
public void setRecognitionCallback(@NonNull @CallbackExecutor Executor executor,
|
||
|
@NonNull RecognitionCallback callback) {
|
||
|
Objects.requireNonNull(callback);
|
||
|
Objects.requireNonNull(executor);
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (mRecognitionCallback == null) {
|
||
|
for (var droppedConsumer : mDroppedConsumerList) {
|
||
|
executor.execute(() -> droppedConsumer.accept(callback));
|
||
|
}
|
||
|
mDroppedConsumerList.clear();
|
||
|
}
|
||
|
mRecognitionCallback = callback;
|
||
|
mRecognitionExecutor = executor;
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Clear the recognition callback associated with this session, if any has been
|
||
|
* set by {@link #setRecognitionCallback(Executor, RecognitionCallback)}.
|
||
|
*/
|
||
|
public void clearRecognitionCallback() {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
mRecognitionCallback = null;
|
||
|
mRecognitionExecutor = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private RecognitionSession(int audioSession,
|
||
|
RecognitionConfig recognitionConfig,
|
||
|
IInjectRecognitionEvent injectRecognitionEvent) {
|
||
|
mAudioSession = audioSession;
|
||
|
mRecognitionConfig = ConversionUtil.aidl2apiRecognitionConfig(recognitionConfig);
|
||
|
mInjectRecognitionEvent = injectRecognitionEvent;
|
||
|
}
|
||
|
|
||
|
private void wrap(Consumer<RecognitionCallback> consumer) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (mRecognitionCallback != null) {
|
||
|
final RecognitionCallback callback = mRecognitionCallback;
|
||
|
mRecognitionExecutor.execute(() -> consumer.accept(callback));
|
||
|
} else {
|
||
|
mDroppedConsumerList.add(consumer);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private final int mAudioSession;
|
||
|
private final SoundTrigger.RecognitionConfig mRecognitionConfig;
|
||
|
private final IInjectRecognitionEvent mInjectRecognitionEvent;
|
||
|
|
||
|
@GuardedBy("SoundTriggerInstrumentation.this.mLock")
|
||
|
private Executor mRecognitionExecutor = null;
|
||
|
@GuardedBy("SoundTriggerInstrumentation.this.mLock")
|
||
|
private RecognitionCallback mRecognitionCallback = null;
|
||
|
@GuardedBy("SoundTriggerInstrumentation.this.mLock")
|
||
|
private final List<Consumer<RecognitionCallback>> mDroppedConsumerList = new ArrayList<>();
|
||
|
}
|
||
|
|
||
|
// Implementation of injection interface passed to the HAL.
|
||
|
// This class will re-associate events received on this callback interface
|
||
|
// with sessions, to avoid staleness issues.
|
||
|
private class Injection extends ISoundTriggerInjection.Stub {
|
||
|
@Override
|
||
|
public void registerGlobalEventInjection(IInjectGlobalEvent globalInjection) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
mInjectGlobalEvent = globalInjection;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onSoundModelLoaded(SoundModel model, @Nullable Phrase[] phrases,
|
||
|
IInjectModelEvent modelInjection, IInjectGlobalEvent globalSession) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return;
|
||
|
ModelSession modelSession = new ModelSession(model, phrases, modelInjection);
|
||
|
mModelSessionMap.put(modelInjection.asBinder(), modelSession);
|
||
|
mGlobalCallbackExecutor.execute(() -> mClientCallback.onModelLoaded(modelSession));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onSoundModelUnloaded(IInjectModelEvent modelSession) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
ModelSession clientModelSession = mModelSessionMap.remove(modelSession.asBinder());
|
||
|
if (clientModelSession == null) return;
|
||
|
clientModelSession.wrap((ModelCallback cb) -> cb.onModelUnloaded());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onRecognitionStarted(int audioSessionHandle, RecognitionConfig config,
|
||
|
IInjectRecognitionEvent recognitionInjection, IInjectModelEvent modelSession) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
ModelSession clientModelSession = mModelSessionMap.get(modelSession.asBinder());
|
||
|
if (clientModelSession == null) return;
|
||
|
RecognitionSession recogSession = new RecognitionSession(
|
||
|
audioSessionHandle, config, recognitionInjection);
|
||
|
mRecognitionSessionMap.put(recognitionInjection.asBinder(), recogSession);
|
||
|
clientModelSession.wrap((ModelCallback cb) ->
|
||
|
cb.onRecognitionStarted(recogSession));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onRecognitionStopped(IInjectRecognitionEvent recognitionSession) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
RecognitionSession clientRecognitionSession =
|
||
|
mRecognitionSessionMap.remove(recognitionSession.asBinder());
|
||
|
if (clientRecognitionSession == null) return;
|
||
|
clientRecognitionSession.wrap((RecognitionCallback cb)
|
||
|
-> cb.onRecognitionStopped());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onParamSet(int modelParam, int value, IInjectModelEvent modelSession) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
ModelSession clientModelSession = mModelSessionMap.get(modelSession.asBinder());
|
||
|
if (clientModelSession == null) return;
|
||
|
clientModelSession.wrap((ModelCallback cb) -> cb.onParamSet(modelParam, value));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
@Override
|
||
|
public void onRestarted(IInjectGlobalEvent globalSession) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return;
|
||
|
mRecognitionSessionMap.clear();
|
||
|
mModelSessionMap.clear();
|
||
|
mGlobalCallbackExecutor.execute(() -> mClientCallback.onRestarted());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onFrameworkDetached(IInjectGlobalEvent globalSession) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return;
|
||
|
mGlobalCallbackExecutor.execute(() -> mClientCallback.onFrameworkDetached());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onClientAttached(IBinder token, IInjectGlobalEvent globalSession) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (globalSession.asBinder() != mInjectGlobalEvent.asBinder()) return;
|
||
|
mClientToken = token;
|
||
|
mGlobalCallbackExecutor.execute(() -> mClientCallback.onClientAttached());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onClientDetached(IBinder token) {
|
||
|
synchronized (SoundTriggerInstrumentation.this.mLock) {
|
||
|
if (token != mClientToken) return;
|
||
|
mClientToken = null;
|
||
|
mGlobalCallbackExecutor.execute(() -> mClientCallback.onClientDetached());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onPreempted() {
|
||
|
// This is always valid, independent of session
|
||
|
mGlobalCallbackExecutor.execute(() -> mClientCallback.onPreempted());
|
||
|
// Callbacks will no longer be delivered, and injection will be silently dropped.
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @hide
|
||
|
*/
|
||
|
@RequiresPermission(android.Manifest.permission.MANAGE_SOUND_TRIGGER)
|
||
|
public SoundTriggerInstrumentation(ISoundTriggerService service,
|
||
|
@CallbackExecutor @NonNull Executor executor,
|
||
|
@NonNull GlobalCallback callback) {
|
||
|
mClientCallback = Objects.requireNonNull(callback);
|
||
|
mGlobalCallbackExecutor = Objects.requireNonNull(executor);
|
||
|
mService = service;
|
||
|
try {
|
||
|
service.attachInjection(new Injection());
|
||
|
} catch (RemoteException e) {
|
||
|
e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Simulate a HAL restart, typically caused by the framework on an unexpected error,
|
||
|
* or a restart of the core audio HAL.
|
||
|
* Application sessions will be detached, and all state will be cleared. The framework
|
||
|
* will re-attach to the HAL following restart.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public void triggerRestart() {
|
||
|
synchronized (mLock) {
|
||
|
if (mInjectGlobalEvent == null) {
|
||
|
throw new IllegalStateException(
|
||
|
"Attempted to trigger HAL restart before registration");
|
||
|
}
|
||
|
try {
|
||
|
mInjectGlobalEvent.triggerRestart();
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Trigger a resource available callback from the fake SoundTrigger HAL to the framework.
|
||
|
* This callback notifies the framework that methods which previously failed due to
|
||
|
* resource contention may now succeed.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public void triggerOnResourcesAvailable() {
|
||
|
synchronized (mLock) {
|
||
|
if (mInjectGlobalEvent == null) {
|
||
|
throw new IllegalStateException(
|
||
|
"Attempted to trigger HAL resources available before registration");
|
||
|
}
|
||
|
try {
|
||
|
mInjectGlobalEvent.triggerOnResourcesAvailable();
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Simulate resource contention, similar to when HAL which does not
|
||
|
* support concurrent capture opens a capture stream, or when a HAL
|
||
|
* has reached its maximum number of models.
|
||
|
* Subsequent model loads and recognition starts will gracefully error.
|
||
|
* Since this call does not trigger a callback through the framework, the
|
||
|
* call will block until the fake HAL has acknowledged the state change.
|
||
|
* @param isResourceContended - true to enable contention, false to return
|
||
|
* to normal functioning.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public void setResourceContention(boolean isResourceContended) {
|
||
|
synchronized (mLock) {
|
||
|
if (mInjectGlobalEvent == null) {
|
||
|
throw new IllegalStateException("Injection interface not set up");
|
||
|
}
|
||
|
IInjectGlobalEvent current = mInjectGlobalEvent;
|
||
|
final CountDownLatch signal = new CountDownLatch(1);
|
||
|
try {
|
||
|
current.setResourceContention(isResourceContended, new IAcknowledgeEvent.Stub() {
|
||
|
@Override
|
||
|
public void eventReceived() {
|
||
|
signal.countDown();
|
||
|
}
|
||
|
});
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
|
||
|
// Block until we get a callback from the service that our request was serviced.
|
||
|
try {
|
||
|
// Rely on test timeout if we don't get a response.
|
||
|
signal.await();
|
||
|
} catch (InterruptedException e) {
|
||
|
throw new RuntimeException(e);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Simulate a phone call for {@link com.android.server.soundtrigger.SoundTriggerService}.
|
||
|
* If the phone call state changes, the service will be notified to respond.
|
||
|
* The service should pause recognition for the duration of the call.
|
||
|
*
|
||
|
* @param isInPhoneCall - {@code true} to cause the SoundTriggerService to
|
||
|
* see the phone call state as off-hook. {@code false} to cause the service to
|
||
|
* see the state as normal.
|
||
|
* @hide
|
||
|
*/
|
||
|
@TestApi
|
||
|
public void setInPhoneCallState(boolean isInPhoneCall) {
|
||
|
try {
|
||
|
mService.setInPhoneCallState(isInPhoneCall);
|
||
|
} catch (RemoteException e) {
|
||
|
throw e.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|