/* * 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. *

* Exception handling: *

*/ 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 mModelCallbacks = new ConcurrentHashMap<>(); // A map from IBinder.DeathRecipient to IHwBinder.DeathRecipient for doing the mapping upon // unlinking. private final @NonNull Map 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 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 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 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. } } }