audio: Add effect attachment to devices and streams

Add the following methods:
  - IModule.{add|remove}DeviceEffect;
  - IStream.{add|remove}Effect.

Bug: 205884982
Test: atest VtsHalAudioCoreTargetTest
Change-Id: I4066e2d10a8e08d634010cfe9eb8f832157e725f
diff --git a/audio/aidl/Android.bp b/audio/aidl/Android.bp
index 674b644..0918971 100644
--- a/audio/aidl/Android.bp
+++ b/audio/aidl/Android.bp
@@ -131,6 +131,7 @@
         "android.hardware.common-V2",
         "android.hardware.common.fmq-V1",
         "android.hardware.audio.common-V1",
+        "android.hardware.audio.effect-V1",
         "android.media.audio.common.types-V2",
     ],
     backend: {
diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl
index ebfa94b..dd2279d 100644
--- a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl
+++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl
@@ -64,6 +64,8 @@
   int generateHwAvSyncId();
   android.hardware.audio.core.VendorParameter[] getVendorParameters(in @utf8InCpp String[] ids);
   void setVendorParameters(in android.hardware.audio.core.VendorParameter[] parameters, boolean async);
+  void addDeviceEffect(int portConfigId, in android.hardware.audio.effect.IEffect effect);
+  void removeDeviceEffect(int portConfigId, in android.hardware.audio.effect.IEffect effect);
   @VintfStability
   parcelable OpenInputStreamArguments {
     int portConfigId;
diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IStreamCommon.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IStreamCommon.aidl
index 8471c79..f0bf100 100644
--- a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IStreamCommon.aidl
+++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IStreamCommon.aidl
@@ -38,4 +38,6 @@
   void updateHwAvSyncId(int hwAvSyncId);
   android.hardware.audio.core.VendorParameter[] getVendorParameters(in @utf8InCpp String[] ids);
   void setVendorParameters(in android.hardware.audio.core.VendorParameter[] parameters, boolean async);
+  void addEffect(in android.hardware.audio.effect.IEffect effect);
+  void removeEffect(in android.hardware.audio.effect.IEffect effect);
 }
diff --git a/audio/aidl/android/hardware/audio/core/IModule.aidl b/audio/aidl/android/hardware/audio/core/IModule.aidl
index 7facc6c..b278ac4 100644
--- a/audio/aidl/android/hardware/audio/core/IModule.aidl
+++ b/audio/aidl/android/hardware/audio/core/IModule.aidl
@@ -30,6 +30,7 @@
 import android.hardware.audio.core.ModuleDebug;
 import android.hardware.audio.core.StreamDescriptor;
 import android.hardware.audio.core.VendorParameter;
+import android.hardware.audio.effect.IEffect;
 import android.media.audio.common.AudioOffloadInfo;
 import android.media.audio.common.AudioPort;
 import android.media.audio.common.AudioPortConfig;
@@ -515,7 +516,8 @@
      * @throws EX_ILLEGAL_ARGUMENT If the port config can not be found by the ID.
      * @throws EX_ILLEGAL_STATE In the following cases:
      *                          - If the port config has a stream opened on it;
-     *                          - If the port config is used by a patch.
+     *                          - If the port config is used by a patch;
+     *                          - If the port config has an audio effect on it.
      */
     void resetAudioPortConfig(int portConfigId);
 
@@ -728,4 +730,34 @@
      * @throws EX_UNSUPPORTED_OPERATION If the module does not support vendor parameters.
      */
     void setVendorParameters(in VendorParameter[] parameters, boolean async);
+
+    /**
+     * Apply an audio effect to a device port.
+     *
+     * The audio effect applies to all audio input or output on the specific
+     * configuration of the device audio port. The effect is inserted according
+     * to its insertion preference specified by the 'flags.insert' field of the
+     * EffectDescriptor.
+     *
+     * @param portConfigId The ID of the audio port config.
+     * @param effect The effect instance.
+     * @throws EX_ILLEGAL_ARGUMENT If the device port config can not be found by the ID,
+     *                             or the effect reference is invalid.
+     * @throws EX_UNSUPPORTED_OPERATION If the module does not support device port effects.
+     */
+    void addDeviceEffect(int portConfigId, in IEffect effect);
+
+    /**
+     * Stop applying an audio effect to a device port.
+     *
+     * Undo the action of the 'addDeviceEffect' method.
+     *
+     * @param portConfigId The ID of the audio port config.
+     * @param effect The effect instance.
+     * @throws EX_ILLEGAL_ARGUMENT If the device port config can not be found by the ID,
+     *                             or the effect reference is invalid, or the effect is
+     *                             not currently applied to the port config.
+     * @throws EX_UNSUPPORTED_OPERATION If the module does not support device port effects.
+     */
+    void removeDeviceEffect(int portConfigId, in IEffect effect);
 }
diff --git a/audio/aidl/android/hardware/audio/core/IStreamCommon.aidl b/audio/aidl/android/hardware/audio/core/IStreamCommon.aidl
index 84f7309..533ef67 100644
--- a/audio/aidl/android/hardware/audio/core/IStreamCommon.aidl
+++ b/audio/aidl/android/hardware/audio/core/IStreamCommon.aidl
@@ -17,6 +17,7 @@
 package android.hardware.audio.core;
 
 import android.hardware.audio.core.VendorParameter;
+import android.hardware.audio.effect.IEffect;
 
 /**
  * This interface contains operations that are common to input and output
@@ -86,4 +87,30 @@
      * @throws EX_UNSUPPORTED_OPERATION If the stream does not support vendor parameters.
      */
     void setVendorParameters(in VendorParameter[] parameters, boolean async);
+
+    /**
+     * Apply an audio effect to the stream.
+     *
+     * This method is intended for the cases when the effect has an offload
+     * implementation, since software effects can be applied at the client side.
+     *
+     * @param effect The effect instance.
+     * @throws EX_ILLEGAL_ARGUMENT If the effect reference is invalid.
+     * @throws EX_ILLEGAL_STATE If the stream is closed.
+     * @throws EX_UNSUPPORTED_OPERATION If the module does not support audio effects.
+     */
+    void addEffect(in IEffect effect);
+
+    /**
+     * Stop applying an audio effect to the stream.
+     *
+     * Undo the action of the 'addEffect' method.
+     *
+     * @param effect The effect instance.
+     * @throws EX_ILLEGAL_ARGUMENT If the effect reference is invalid, or the effect is
+     *                             not currently applied to the stream.
+     * @throws EX_ILLEGAL_STATE If the stream is closed.
+     * @throws EX_UNSUPPORTED_OPERATION If the module does not support audio effects.
+     */
+    void removeEffect(in IEffect effect);
 }
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index d52e328..4a424bd 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -969,4 +969,28 @@
     return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
 }
 
+ndk::ScopedAStatus Module::addDeviceEffect(
+        int32_t in_portConfigId,
+        const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect) {
+    if (in_effect == nullptr) {
+        LOG(DEBUG) << __func__ << ": port id " << in_portConfigId << ", null effect";
+    } else {
+        LOG(DEBUG) << __func__ << ": port id " << in_portConfigId << ", effect Binder "
+                   << in_effect->asBinder().get();
+    }
+    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ndk::ScopedAStatus Module::removeDeviceEffect(
+        int32_t in_portConfigId,
+        const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect) {
+    if (in_effect == nullptr) {
+        LOG(DEBUG) << __func__ << ": port id " << in_portConfigId << ", null effect";
+    } else {
+        LOG(DEBUG) << __func__ << ": port id " << in_portConfigId << ", effect Binder "
+                   << in_effect->asBinder().get();
+    }
+    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
 }  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/Stream.cpp b/audio/aidl/default/Stream.cpp
index e984091..bb123a2 100644
--- a/audio/aidl/default/Stream.cpp
+++ b/audio/aidl/default/Stream.cpp
@@ -541,6 +541,28 @@
 }
 
 template <class Metadata, class StreamWorker>
+ndk::ScopedAStatus StreamCommonImpl<Metadata, StreamWorker>::addEffect(
+        const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect) {
+    if (in_effect == nullptr) {
+        LOG(DEBUG) << __func__ << ": null effect";
+    } else {
+        LOG(DEBUG) << __func__ << ": effect Binder" << in_effect->asBinder().get();
+    }
+    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+template <class Metadata, class StreamWorker>
+ndk::ScopedAStatus StreamCommonImpl<Metadata, StreamWorker>::removeEffect(
+        const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect) {
+    if (in_effect == nullptr) {
+        LOG(DEBUG) << __func__ << ": null effect";
+    } else {
+        LOG(DEBUG) << __func__ << ": effect Binder" << in_effect->asBinder().get();
+    }
+    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+template <class Metadata, class StreamWorker>
 ndk::ScopedAStatus StreamCommonImpl<Metadata, StreamWorker>::close() {
     LOG(DEBUG) << __func__;
     if (!isClosed()) {
diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h
index 6baaa76..aa05d2a 100644
--- a/audio/aidl/default/include/core-impl/Module.h
+++ b/audio/aidl/default/include/core-impl/Module.h
@@ -92,6 +92,14 @@
                                            std::vector<VendorParameter>* _aidl_return) override;
     ndk::ScopedAStatus setVendorParameters(const std::vector<VendorParameter>& in_parameters,
                                            bool in_async) override;
+    ndk::ScopedAStatus addDeviceEffect(
+            int32_t in_portConfigId,
+            const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect)
+            override;
+    ndk::ScopedAStatus removeDeviceEffect(
+            int32_t in_portConfigId,
+            const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect)
+            override;
 
     void cleanUpPatch(int32_t patchId);
     ndk::ScopedAStatus createStreamContext(
diff --git a/audio/aidl/default/include/core-impl/Stream.h b/audio/aidl/default/include/core-impl/Stream.h
index e8b2c54..a5d240f 100644
--- a/audio/aidl/default/include/core-impl/Stream.h
+++ b/audio/aidl/default/include/core-impl/Stream.h
@@ -212,6 +212,12 @@
                                                    std::vector<VendorParameter>* _aidl_return) = 0;
     virtual ndk::ScopedAStatus setVendorParameters(
             const std::vector<VendorParameter>& in_parameters, bool in_async) = 0;
+    virtual ndk::ScopedAStatus addEffect(
+            const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>&
+                    in_effect) = 0;
+    virtual ndk::ScopedAStatus removeEffect(
+            const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>&
+                    in_effect) = 0;
 };
 
 class StreamCommon : public BnStreamCommon {
@@ -242,6 +248,20 @@
         return delegate != nullptr ? delegate->setVendorParameters(in_parameters, in_async)
                                    : ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     }
+    ndk::ScopedAStatus addEffect(
+            const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect)
+            override {
+        auto delegate = mDelegate.lock();
+        return delegate != nullptr ? delegate->addEffect(in_effect)
+                                   : ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    ndk::ScopedAStatus removeEffect(
+            const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect)
+            override {
+        auto delegate = mDelegate.lock();
+        return delegate != nullptr ? delegate->removeEffect(in_effect)
+                                   : ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
     // It is possible that on the client side the proxy for IStreamCommon will outlive
     // the IStream* instance, and the server side IStream* instance will get destroyed
     // while this IStreamCommon instance is still alive.
@@ -257,6 +277,12 @@
                                            std::vector<VendorParameter>* _aidl_return) override;
     ndk::ScopedAStatus setVendorParameters(const std::vector<VendorParameter>& in_parameters,
                                            bool in_async) override;
+    ndk::ScopedAStatus addEffect(
+            const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect)
+            override;
+    ndk::ScopedAStatus removeEffect(
+            const std::shared_ptr<::aidl::android::hardware::audio::effect::IEffect>& in_effect)
+            override;
 
     ndk::ScopedAStatus getStreamCommon(std::shared_ptr<IStreamCommon>* _aidl_return);
     ndk::ScopedAStatus init() {
diff --git a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
index 50fb981..4e3f525 100644
--- a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
@@ -1746,6 +1746,33 @@
     }
 }
 
+// See b/262930731. In the absence of offloaded effect implementations,
+// currently we can only pass a nullptr, and the HAL module must either reject
+// it as an invalid argument, or say that offloaded effects are not supported.
+TEST_P(AudioCoreModule, AddRemoveEffectInvalidArguments) {
+    ndk::ScopedAStatus addEffectStatus = module->addDeviceEffect(-1, nullptr);
+    ndk::ScopedAStatus removeEffectStatus = module->removeDeviceEffect(-1, nullptr);
+    const bool isSupported = addEffectStatus.getExceptionCode() != EX_UNSUPPORTED_OPERATION;
+    if (isSupported) {
+        EXPECT_EQ(EX_ILLEGAL_ARGUMENT, addEffectStatus.getExceptionCode());
+        EXPECT_EQ(EX_ILLEGAL_ARGUMENT, removeEffectStatus.getExceptionCode());
+    } else if (EX_UNSUPPORTED_OPERATION != removeEffectStatus.getExceptionCode()) {
+        GTEST_FAIL() << "addEffect and removeEffect must be either supported or not supported "
+                     << "together";
+    } else {
+        GTEST_SKIP() << "Offloaded effects not supported";
+    }
+    // Test rejection of a nullptr effect with a valid device port Id.
+    ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
+    const auto configs = moduleConfig->getPortConfigsForAttachedDevicePorts();
+    for (const auto& config : configs) {
+        WithAudioPortConfig portConfig(config);
+        ASSERT_NO_FATAL_FAILURE(portConfig.SetUp(module.get()));
+        EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, module->addDeviceEffect(portConfig.getId(), nullptr));
+        EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, module->removeDeviceEffect(portConfig.getId(), nullptr));
+    }
+}
+
 class AudioCoreTelephony : public AudioCoreModuleBase, public testing::TestWithParam<std::string> {
   public:
     void SetUp() override {
@@ -2080,6 +2107,43 @@
         }
     }
 
+    // See b/262930731. In the absence of offloaded effect implementations,
+    // currently we can only pass a nullptr, and the HAL module must either reject
+    // it as an invalid argument, or say that offloaded effects are not supported.
+    void AddRemoveEffectInvalidArguments() {
+        const auto ports =
+                moduleConfig->getMixPorts(IOTraits<Stream>::is_input, false /*attachedOnly*/);
+        if (ports.empty()) {
+            GTEST_SKIP() << "No mix ports";
+        }
+        bool atLeastOneSupports = false;
+        for (const auto& port : ports) {
+            const auto portConfig = moduleConfig->getSingleConfigForMixPort(true, port);
+            if (!portConfig.has_value()) continue;
+            WithStream<Stream> stream(portConfig.value());
+            ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+            std::shared_ptr<IStreamCommon> streamCommon;
+            ASSERT_IS_OK(stream.get()->getStreamCommon(&streamCommon));
+            ASSERT_NE(nullptr, streamCommon);
+            ndk::ScopedAStatus addEffectStatus = streamCommon->addEffect(nullptr);
+            ndk::ScopedAStatus removeEffectStatus = streamCommon->removeEffect(nullptr);
+            const bool isSupported = addEffectStatus.getExceptionCode() != EX_UNSUPPORTED_OPERATION;
+            if (isSupported) {
+                EXPECT_EQ(EX_ILLEGAL_ARGUMENT, addEffectStatus.getExceptionCode());
+                EXPECT_EQ(EX_ILLEGAL_ARGUMENT, removeEffectStatus.getExceptionCode());
+                atLeastOneSupports = true;
+            } else if (EX_UNSUPPORTED_OPERATION != removeEffectStatus.getExceptionCode()) {
+                ADD_FAILURE()
+                        << "addEffect and removeEffect must be either supported or not supported "
+                        << "together";
+                atLeastOneSupports = true;
+            }
+        }
+        if (!atLeastOneSupports) {
+            GTEST_SKIP() << "Offloaded effects not supported";
+        }
+    }
+
     void OpenTwiceSamePortConfigImpl(const AudioPortConfig& portConfig) {
         WithStream<Stream> stream1(portConfig);
         ASSERT_NO_FATAL_FAILURE(stream1.SetUp(module.get(), kDefaultBufferSizeFrames));
@@ -2157,6 +2221,7 @@
 TEST_IN_AND_OUT_STREAM(GetVendorParameters);
 TEST_IN_AND_OUT_STREAM(SetVendorParameters);
 TEST_IN_AND_OUT_STREAM(HwGainHwVolume);
+TEST_IN_AND_OUT_STREAM(AddRemoveEffectInvalidArguments);
 
 namespace aidl::android::hardware::audio::core {
 std::ostream& operator<<(std::ostream& os, const IStreamIn::MicrophoneDirection& md) {