blob: 730e92cb2aeeb53eb8beb8d9c5bb8f290ec13b4b [file] [log] [blame]
/*
* Copyright (C) 2019 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.NonNull;
import android.annotation.Nullable;
import android.hardware.soundtrigger.V2_0.ISoundTriggerHw;
import android.media.soundtrigger.ModelParameterRange;
import android.media.soundtrigger.PhraseSoundModel;
import android.media.soundtrigger.Properties;
import android.media.soundtrigger.RecognitionConfig;
import android.media.soundtrigger.SoundModel;
import android.media.soundtrigger.Status;
import android.media.soundtrigger_middleware.PhraseRecognitionEventSys;
import android.media.soundtrigger_middleware.RecognitionEventSys;
import android.os.IBinder;
import android.os.IHwBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.system.OsConstants;
import android.util.Slog;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* An implementation of {@link ISoundTriggerHal}, on top of any
* android.hardware.soundtrigger.V2_x.ISoundTriggerHw implementation. This class hides away some of
* the details involved with retaining backward compatibility and adapts to the more pleasant syntax
* exposed by {@link ISoundTriggerHal}, compared to the bare driver interface.
* <p>
* Exception handling:
* <ul>
* <li>All {@link RemoteException}s get rethrown as {@link RuntimeException}.
* <li>All HAL malfunctions get thrown as {@link HalException}.
* <li>All unsupported operations get thrown as {@link RecoverableException} with a
* {@link android.media.soundtrigger.Status#OPERATION_NOT_SUPPORTED}
* code.
* </ul>
*/
final class SoundTriggerHw2Compat implements ISoundTriggerHal {
private static final String TAG = "SoundTriggerHw2Compat";
private final @NonNull Runnable mRebootRunnable;
private final @NonNull IHwBinder mBinder;
private @NonNull android.hardware.soundtrigger.V2_0.ISoundTriggerHw mUnderlying_2_0;
private @Nullable android.hardware.soundtrigger.V2_1.ISoundTriggerHw mUnderlying_2_1;
private @Nullable android.hardware.soundtrigger.V2_2.ISoundTriggerHw mUnderlying_2_2;
private @Nullable android.hardware.soundtrigger.V2_3.ISoundTriggerHw mUnderlying_2_3;
// HAL <=2.1 requires us to pass a callback argument to startRecognition. We will store the one
// passed on load and then pass it on start. We don't bother storing the callback on newer
// versions.
private final @NonNull ConcurrentMap<Integer, ModelCallback> mModelCallbacks =
new ConcurrentHashMap<>();
// A map from IBinder.DeathRecipient to IHwBinder.DeathRecipient for doing the mapping upon
// unlinking.
private final @NonNull Map<IBinder.DeathRecipient, IHwBinder.DeathRecipient>
mDeathRecipientMap = new HashMap<>();
// The properties are read at construction time and cached, since we need to use some of them
// to enforce constraints.
private final @NonNull Properties mProperties;
static ISoundTriggerHal create(
@NonNull ISoundTriggerHw underlying,
@NonNull Runnable rebootRunnable,
ICaptureStateNotifier notifier) {
return create(underlying.asBinder(), rebootRunnable, notifier);
}
static ISoundTriggerHal create(@NonNull IHwBinder binder,
@NonNull Runnable rebootRunnable,
ICaptureStateNotifier notifier) {
SoundTriggerHw2Compat compat = new SoundTriggerHw2Compat(binder, rebootRunnable);
ISoundTriggerHal result = compat;
// Add max model limiter for versions.
result = new SoundTriggerHalMaxModelLimiter(result, compat.mProperties.maxSoundModels);
// Add concurrent capture handler for HALs which do not support concurrent capture.
if (!compat.mProperties.concurrentCapture) {
result = new SoundTriggerHalConcurrentCaptureHandler(result, notifier);
}
return result;
}
private SoundTriggerHw2Compat(@NonNull IHwBinder binder, @NonNull Runnable rebootRunnable) {
mRebootRunnable = Objects.requireNonNull(rebootRunnable);
mBinder = Objects.requireNonNull(binder);
initUnderlying(binder);
mProperties = Objects.requireNonNull(getPropertiesInternal());
}
private void initUnderlying(IHwBinder binder) {
// We want to share the proxy instances rather than create a separate proxy for every
// version, so we go down the versions in descending order to find the latest one supported,
// and then simply up-cast it to obtain all the versions that are earlier.
// Attempt 2.3
android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3 =
android.hardware.soundtrigger.V2_3.ISoundTriggerHw.asInterface(binder);
if (as2_3 != null) {
mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = as2_3;
return;
}
// Attempt 2.2
android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2 =
android.hardware.soundtrigger.V2_2.ISoundTriggerHw.asInterface(binder);
if (as2_2 != null) {
mUnderlying_2_0 = mUnderlying_2_1 = mUnderlying_2_2 = as2_2;
mUnderlying_2_3 = null;
return;
}
// Attempt 2.1
android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1 =
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.asInterface(binder);
if (as2_1 != null) {
mUnderlying_2_0 = mUnderlying_2_1 = as2_1;
mUnderlying_2_2 = mUnderlying_2_3 = null;
return;
}
// Attempt 2.0
android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0 =
android.hardware.soundtrigger.V2_0.ISoundTriggerHw.asInterface(binder);
if (as2_0 != null) {
mUnderlying_2_0 = as2_0;
mUnderlying_2_1 = mUnderlying_2_2 = mUnderlying_2_3 = null;
return;
}
throw new RuntimeException("Binder doesn't support ISoundTriggerHw@2.0");
}
private static void handleHalStatus(int status, String methodName) {
if (status != 0) {
throw new HalException(status, methodName);
}
}
private static void handleHalStatusAllowBusy(int status, String methodName) {
if (status == -OsConstants.EBUSY) {
throw new RecoverableException(Status.RESOURCE_CONTENTION);
}
handleHalStatus(status, methodName);
}
@Override
public void reboot() {
mRebootRunnable.run();
}
@Override
public void detach() {
// No-op.
}
@Override
public Properties getProperties() {
return mProperties;
}
private Properties getPropertiesInternal() {
try {
AtomicInteger retval = new AtomicInteger(-1);
AtomicReference<android.hardware.soundtrigger.V2_3.Properties> properties =
new AtomicReference<>();
try {
as2_3().getProperties_2_3(
(r, p) -> {
retval.set(r);
properties.set(p);
});
} catch (NotSupported e) {
// Fall-back to the 2.0 version:
return getProperties_2_0();
}
handleHalStatus(retval.get(), "getProperties_2_3");
return ConversionUtil.hidl2aidlProperties(properties.get());
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
@Override
public void registerCallback(GlobalCallback callback) {
// In versions 2.x the events represented by this callback don't exist, we can
// safely ignore this.
}
@Override
public int loadSoundModel(SoundModel soundModel, ModelCallback callback) {
android.hardware.soundtrigger.V2_3.ISoundTriggerHw.SoundModel hidlModel =
ConversionUtil.aidl2hidlSoundModel(soundModel);
try {
AtomicInteger retval = new AtomicInteger(-1);
AtomicInteger handle = new AtomicInteger(0);
try {
as2_1().loadSoundModel_2_1(hidlModel, new ModelCallbackWrapper(callback),
0,
(r, h) -> {
retval.set(r);
handle.set(h);
});
handleHalStatus(retval.get(), "loadSoundModel_2_1");
mModelCallbacks.put(handle.get(), callback);
} catch (NotSupported ee) {
// Fall-back to the 2.0 version:
return loadSoundModel_2_0(hidlModel, callback);
}
return handle.get();
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} finally {
// TODO(b/219825762): We should be able to use the entire object in a try-with-resources
// clause, instead of having to explicitly close internal fields.
if (hidlModel.data != null) {
try {
hidlModel.data.close();
} catch (IOException e) {
Slog.e(TAG, "Failed to close file", e);
}
}
}
}
@Override
public int loadPhraseSoundModel(PhraseSoundModel soundModel, ModelCallback callback) {
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel hidlModel =
ConversionUtil.aidl2hidlPhraseSoundModel(soundModel);
try {
AtomicInteger retval = new AtomicInteger(-1);
AtomicInteger handle = new AtomicInteger(0);
try {
as2_1().loadPhraseSoundModel_2_1(hidlModel, new ModelCallbackWrapper(callback),
0,
(r, h) -> {
retval.set(r);
handle.set(h);
});
handleHalStatus(retval.get(), "loadPhraseSoundModel_2_1");
mModelCallbacks.put(handle.get(), callback);
} catch (NotSupported ee) {
// Fall-back to the 2.0 version:
return loadPhraseSoundModel_2_0(hidlModel, callback);
}
return handle.get();
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} finally {
// TODO(b/219825762): We should be able to use the entire object in a try-with-resources
// clause, instead of having to explicitly close internal fields.
if (hidlModel.common.data != null) {
try {
hidlModel.common.data.close();
} catch (IOException e) {
Slog.e(TAG, "Failed to close file", e);
}
}
}
}
@Override
public void unloadSoundModel(int modelHandle) {
try {
// Safe if key doesn't exist.
mModelCallbacks.remove(modelHandle);
int retval = as2_0().unloadSoundModel(modelHandle);
handleHalStatus(retval, "unloadSoundModel");
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
@Override
public void stopRecognition(int modelHandle) {
try {
int retval = as2_0().stopRecognition(modelHandle);
handleHalStatus(retval, "stopRecognition");
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
@Override
public void startRecognition(int modelHandle, int deviceHandle, int ioHandle,
RecognitionConfig config) {
android.hardware.soundtrigger.V2_3.RecognitionConfig hidlConfig =
ConversionUtil.aidl2hidlRecognitionConfig(config, deviceHandle, ioHandle);
try {
try {
int retval = as2_3().startRecognition_2_3(modelHandle, hidlConfig);
handleHalStatus(retval, "startRecognition_2_3");
} catch (NotSupported ee) {
// Fall-back to the 2.0 version:
startRecognition_2_1(modelHandle, hidlConfig);
}
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
@Override
public void forceRecognitionEvent(int modelHandle) {
try {
int retval = as2_2().getModelState(modelHandle);
handleHalStatus(retval, "getModelState");
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} catch (NotSupported e) {
throw e.throwAsRecoverableException();
}
}
@Override
public int getModelParameter(int modelHandle, int param) {
AtomicInteger status = new AtomicInteger(-1);
AtomicInteger value = new AtomicInteger(0);
try {
as2_3().getParameter(modelHandle, param,
(s, v) -> {
status.set(s);
value.set(v);
});
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} catch (NotSupported e) {
throw e.throwAsRecoverableException();
}
handleHalStatus(status.get(), "getParameter");
return value.get();
}
@Override
public void setModelParameter(int modelHandle, int param, int value) {
try {
int retval = as2_3().setParameter(modelHandle, param, value);
handleHalStatus(retval, "setParameter");
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
} catch (NotSupported e) {
throw e.throwAsRecoverableException();
}
}
@Override
public ModelParameterRange queryParameter(int modelHandle, int param) {
AtomicInteger status = new AtomicInteger(-1);
AtomicReference<android.hardware.soundtrigger.V2_3.OptionalModelParameterRange>
optionalRange =
new AtomicReference<>();
try {
as2_3().queryParameter(modelHandle, param,
(s, r) -> {
status.set(s);
optionalRange.set(r);
});
} catch (NotSupported e) {
// For older drivers, we consider no model parameter to be supported.
return null;
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
handleHalStatus(status.get(), "queryParameter");
return (optionalRange.get().getDiscriminator()
== android.hardware.soundtrigger.V2_3.OptionalModelParameterRange.hidl_discriminator.range)
?
ConversionUtil.hidl2aidlModelParameterRange(optionalRange.get().range()) : null;
}
@Override
public void linkToDeath(IBinder.DeathRecipient recipient) {
IHwBinder.DeathRecipient wrapper = cookie -> recipient.binderDied();
mDeathRecipientMap.put(recipient, wrapper);
mBinder.linkToDeath(wrapper, 0);
}
@Override
public void unlinkToDeath(IBinder.DeathRecipient recipient) {
mBinder.unlinkToDeath(mDeathRecipientMap.remove(recipient));
}
@Override
public String interfaceDescriptor() {
try {
return as2_0().interfaceDescriptor();
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
@Override
public void flushCallbacks() {
// 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);
AtomicReference<android.hardware.soundtrigger.V2_0.ISoundTriggerHw.Properties>
properties =
new AtomicReference<>();
as2_0().getProperties(
(r, p) -> {
retval.set(r);
properties.set(p);
});
handleHalStatus(retval.get(), "getProperties");
return ConversionUtil.hidl2aidlProperties(
Hw2CompatUtil.convertProperties_2_0_to_2_3(properties.get()));
}
private int loadSoundModel_2_0(
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.SoundModel soundModel,
ModelCallback callback)
throws RemoteException {
// Convert the soundModel to V2.0.
android.hardware.soundtrigger.V2_0.ISoundTriggerHw.SoundModel model_2_0 =
Hw2CompatUtil.convertSoundModel_2_1_to_2_0(soundModel);
AtomicInteger retval = new AtomicInteger(-1);
AtomicInteger handle = new AtomicInteger(0);
as2_0().loadSoundModel(model_2_0, new ModelCallbackWrapper(callback), 0, (r, h) -> {
retval.set(r);
handle.set(h);
});
handleHalStatus(retval.get(), "loadSoundModel");
mModelCallbacks.put(handle.get(), callback);
return handle.get();
}
private int loadPhraseSoundModel_2_0(
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.PhraseSoundModel soundModel,
ModelCallback callback)
throws RemoteException {
// Convert the soundModel to V2.0.
android.hardware.soundtrigger.V2_0.ISoundTriggerHw.PhraseSoundModel model_2_0 =
Hw2CompatUtil.convertPhraseSoundModel_2_1_to_2_0(soundModel);
AtomicInteger retval = new AtomicInteger(-1);
AtomicInteger handle = new AtomicInteger(0);
as2_0().loadPhraseSoundModel(model_2_0, new ModelCallbackWrapper(callback), 0,
(r, h) -> {
retval.set(r);
handle.set(h);
});
handleHalStatus(retval.get(), "loadSoundModel");
mModelCallbacks.put(handle.get(), callback);
return handle.get();
}
private void startRecognition_2_1(int modelHandle,
android.hardware.soundtrigger.V2_3.RecognitionConfig config) {
try {
try {
android.hardware.soundtrigger.V2_1.ISoundTriggerHw.RecognitionConfig config_2_1 =
Hw2CompatUtil.convertRecognitionConfig_2_3_to_2_1(config);
int retval = as2_1().startRecognition_2_1(modelHandle, config_2_1,
new ModelCallbackWrapper(mModelCallbacks.get(modelHandle)), 0);
handleHalStatus(retval, "startRecognition_2_1");
} catch (NotSupported e) {
// Fall-back to the 2.0 version:
startRecognition_2_0(modelHandle, config);
}
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
}
}
private void startRecognition_2_0(int modelHandle,
android.hardware.soundtrigger.V2_3.RecognitionConfig config)
throws RemoteException {
android.hardware.soundtrigger.V2_0.ISoundTriggerHw.RecognitionConfig config_2_0 =
Hw2CompatUtil.convertRecognitionConfig_2_3_to_2_0(config);
int retval = as2_0().startRecognition(modelHandle, config_2_0,
new ModelCallbackWrapper(mModelCallbacks.get(modelHandle)), 0);
handleHalStatus(retval, "startRecognition");
}
private @NonNull
android.hardware.soundtrigger.V2_0.ISoundTriggerHw as2_0() {
return mUnderlying_2_0;
}
private @NonNull
android.hardware.soundtrigger.V2_1.ISoundTriggerHw as2_1() throws NotSupported {
if (mUnderlying_2_1 == null) {
throw new NotSupported("Underlying driver version < 2.1");
}
return mUnderlying_2_1;
}
private @NonNull
android.hardware.soundtrigger.V2_2.ISoundTriggerHw as2_2() throws NotSupported {
if (mUnderlying_2_2 == null) {
throw new NotSupported("Underlying driver version < 2.2");
}
return mUnderlying_2_2;
}
private @NonNull
android.hardware.soundtrigger.V2_3.ISoundTriggerHw as2_3() throws NotSupported {
if (mUnderlying_2_3 == null) {
throw new NotSupported("Underlying driver version < 2.3");
}
return mUnderlying_2_3;
}
/**
* A checked exception representing the requested interface version not being supported.
* At the public interface layer, use {@link #throwAsRecoverableException()} to propagate it to
* the caller if the request cannot be fulfilled.
*/
private static class NotSupported extends Exception {
NotSupported(String message) {
super(message);
}
/**
* Throw this as a recoverable exception.
*
* @return Never actually returns anything. Always throws. Used so that caller can write
* throw e.throwAsRecoverableException().
*/
RecoverableException throwAsRecoverableException() {
throw new RecoverableException(Status.OPERATION_NOT_SUPPORTED, getMessage());
}
}
private static class ModelCallbackWrapper extends
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.Stub {
private final @NonNull ModelCallback mDelegate;
private ModelCallbackWrapper(
@NonNull ModelCallback delegate) {
mDelegate = Objects.requireNonNull(delegate);
}
@Override
public void recognitionCallback_2_1(
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event,
int cookie) {
RecognitionEventSys eventSys = new RecognitionEventSys();
eventSys.recognitionEvent = ConversionUtil.hidl2aidlRecognitionEvent(event);
eventSys.halEventReceivedMillis = SystemClock.elapsedRealtime();
mDelegate.recognitionCallback(event.header.model, eventSys);
}
@Override
public void phraseRecognitionCallback_2_1(
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent event,
int cookie) {
PhraseRecognitionEventSys eventSys = new PhraseRecognitionEventSys();
eventSys.phraseRecognitionEvent = ConversionUtil.hidl2aidlPhraseRecognitionEvent(event);
eventSys.halEventReceivedMillis = SystemClock.elapsedRealtime();
mDelegate.phraseRecognitionCallback(event.common.header.model, eventSys);
}
@Override
public void soundModelCallback_2_1(
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.ModelEvent event,
int cookie) {
// Nobody cares.
}
@Override
public void recognitionCallback(
android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.RecognitionEvent event,
int cookie) {
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.RecognitionEvent event_2_1 =
Hw2CompatUtil.convertRecognitionEvent_2_0_to_2_1(event);
recognitionCallback_2_1(event_2_1, cookie);
}
@Override
public void phraseRecognitionCallback(
android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.PhraseRecognitionEvent event,
int cookie) {
android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback.PhraseRecognitionEvent
event_2_1 = Hw2CompatUtil.convertPhraseRecognitionEvent_2_0_to_2_1(event);
phraseRecognitionCallback_2_1(event_2_1, cookie);
}
@Override
public void soundModelCallback(
android.hardware.soundtrigger.V2_0.ISoundTriggerHwCallback.ModelEvent event,
int cookie) {
// Nobody cares.
}
}
}