AIDL: Add unit tests to external device disconnection

Add tests for the code added in aosp/3005114.
Fix a minor issue found. It can lead to accumulating
entries in `mFwkPatches` for "implicit" patches.
This should rarely occur in practice as the framework
usually creates explicit patches for external devices.

Bug: 327449747
Test: atest CoreAudioHalAidlTest
Change-Id: I9c4a045bb5dba2b33acb65f7aba1a447a3dac4d0
diff --git a/media/libaudiohal/impl/Hal2AidlMapper.cpp b/media/libaudiohal/impl/Hal2AidlMapper.cpp
index acc69ec..263e3e9 100644
--- a/media/libaudiohal/impl/Hal2AidlMapper.cpp
+++ b/media/libaudiohal/impl/Hal2AidlMapper.cpp
@@ -450,7 +450,7 @@
         *portConfig = it->second;
         return OK;
     }
-    ALOGE("%s: could not find a configured device port for device %s",
+    ALOGE("%s: could not find a device port config for device %s",
             __func__, device.toString().c_str());
     return BAD_VALUE;
 }
@@ -1020,6 +1020,13 @@
                 }
             }
             resetUnusedPortConfigs();
+            // Patches created by Hal2AidlMapper during stream creation and not "claimed"
+            // by the framework must not be surfaced to it.
+            for (auto& s : mStreams) {
+                if (auto it = releasedPatches.find(s.second.second); it != releasedPatches.end()) {
+                    releasedPatches.erase(it);
+                }
+            }
             mFwkPatches.merge(releasedPatches);
             LOG_ALWAYS_FATAL_IF(!releasedPatches.empty(),
                     "mFwkPatches already contains some of released patches");
diff --git a/media/libaudiohal/tests/CoreAudioHalAidl_test.cpp b/media/libaudiohal/tests/CoreAudioHalAidl_test.cpp
index 3541078..5106874 100644
--- a/media/libaudiohal/tests/CoreAudioHalAidl_test.cpp
+++ b/media/libaudiohal/tests/CoreAudioHalAidl_test.cpp
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
+#include <algorithm>
 #include <memory>
+#include <mutex>
 #include <string>
 #include <vector>
 
@@ -22,6 +24,7 @@
 #include <gtest/gtest.h>
 
 #include <DeviceHalAidl.h>
+#include <Hal2AidlMapper.h>
 #include <StreamHalAidl.h>
 #include <aidl/android/hardware/audio/core/BnModule.h>
 #include <aidl/android/hardware/audio/core/BnStreamCommon.h>
@@ -31,7 +34,24 @@
 
 namespace {
 
+using ::aidl::android::hardware::audio::core::AudioPatch;
+using ::aidl::android::hardware::audio::core::AudioRoute;
 using ::aidl::android::hardware::audio::core::VendorParameter;
+using ::aidl::android::media::audio::common::AudioChannelLayout;
+using ::aidl::android::media::audio::common::AudioConfig;
+using ::aidl::android::media::audio::common::AudioDeviceDescription;
+using ::aidl::android::media::audio::common::AudioDeviceType;
+using ::aidl::android::media::audio::common::AudioFormatDescription;
+using ::aidl::android::media::audio::common::AudioFormatType;
+using ::aidl::android::media::audio::common::AudioIoFlags;
+using ::aidl::android::media::audio::common::AudioPort;
+using ::aidl::android::media::audio::common::AudioPortConfig;
+using ::aidl::android::media::audio::common::AudioPortDeviceExt;
+using ::aidl::android::media::audio::common::AudioPortExt;
+using ::aidl::android::media::audio::common::AudioPortMixExt;
+using ::aidl::android::media::audio::common::AudioProfile;
+using ::aidl::android::media::audio::common::AudioSource;
+using ::aidl::android::media::audio::common::PcmType;
 
 class VendorParameterMock {
   public:
@@ -63,9 +83,105 @@
     std::vector<VendorParameter> mSyncParameters;
 };
 
+struct Configuration {
+    std::vector<AudioPort> ports;
+    std::vector<AudioPortConfig> portConfigs;
+    std::vector<AudioRoute> routes;
+    std::vector<AudioPatch> patches;
+    int32_t nextPortId = 1;
+    int32_t nextPatchId = 1;
+};
+
+void fillProfile(AudioProfile* profile, const std::vector<int32_t>& channelLayouts,
+                 const std::vector<int32_t>& sampleRates) {
+    for (auto layout : channelLayouts) {
+        profile->channelMasks.push_back(
+                AudioChannelLayout::make<AudioChannelLayout::layoutMask>(layout));
+    }
+    profile->sampleRates.insert(profile->sampleRates.end(), sampleRates.begin(), sampleRates.end());
+}
+
+AudioProfile createProfile(PcmType pcmType, const std::vector<int32_t>& channelLayouts,
+                           const std::vector<int32_t>& sampleRates) {
+    AudioProfile profile;
+    profile.format.type = AudioFormatType::PCM;
+    profile.format.pcm = pcmType;
+    fillProfile(&profile, channelLayouts, sampleRates);
+    return profile;
+}
+
+AudioPortExt createPortDeviceExt(AudioDeviceType devType, int32_t flags,
+                                 std::string connection = "") {
+    AudioPortDeviceExt deviceExt;
+    deviceExt.device.type.type = devType;
+    if (devType == AudioDeviceType::IN_MICROPHONE && connection.empty()) {
+        deviceExt.device.address = "bottom";
+    } else if (devType == AudioDeviceType::IN_MICROPHONE_BACK && connection.empty()) {
+        deviceExt.device.address = "back";
+    }
+    deviceExt.device.type.connection = std::move(connection);
+    deviceExt.flags = flags;
+    return AudioPortExt::make<AudioPortExt::device>(deviceExt);
+}
+
+AudioPortExt createPortMixExt(int32_t maxOpenStreamCount, int32_t maxActiveStreamCount) {
+    AudioPortMixExt mixExt;
+    mixExt.maxOpenStreamCount = maxOpenStreamCount;
+    mixExt.maxActiveStreamCount = maxActiveStreamCount;
+    return AudioPortExt::make<AudioPortExt::mix>(mixExt);
+}
+
+AudioPort createPort(int32_t id, const std::string& name, int32_t flags, bool isInput,
+                     const AudioPortExt& ext) {
+    AudioPort port;
+    port.id = id;
+    port.name = name;
+    port.flags = isInput ? AudioIoFlags::make<AudioIoFlags::input>(flags)
+                         : AudioIoFlags::make<AudioIoFlags::output>(flags);
+    port.ext = ext;
+    return port;
+}
+
+AudioRoute createRoute(const std::vector<AudioPort>& sources, const AudioPort& sink) {
+    AudioRoute route;
+    route.sinkPortId = sink.id;
+    std::transform(sources.begin(), sources.end(), std::back_inserter(route.sourcePortIds),
+                   [](const auto& port) { return port.id; });
+    return route;
+}
+
+template <typename T>
+auto findById(std::vector<T>& v, int32_t id) {
+    return std::find_if(v.begin(), v.end(), [&](const auto& e) { return e.id == id; });
+}
+
+Configuration getTestConfiguration() {
+    const std::vector<AudioProfile> standardPcmAudioProfiles = {
+            createProfile(PcmType::INT_16_BIT, {AudioChannelLayout::LAYOUT_STEREO}, {48000})};
+    Configuration c;
+
+    AudioPort btOutDevice =
+            createPort(c.nextPortId++, "BT A2DP Out", 0, false,
+                       createPortDeviceExt(AudioDeviceType::OUT_DEVICE, 0,
+                                           AudioDeviceDescription::CONNECTION_BT_A2DP));
+    btOutDevice.profiles = standardPcmAudioProfiles;
+    c.ports.push_back(btOutDevice);
+
+    AudioPort btOutMix =
+            createPort(c.nextPortId++, "a2dp output", 0, false, createPortMixExt(1, 1));
+    btOutMix.profiles = standardPcmAudioProfiles;
+    c.ports.push_back(btOutMix);
+
+    c.routes.push_back(createRoute({btOutMix}, btOutDevice));
+
+    return c;
+}
+
 class ModuleMock : public ::aidl::android::hardware::audio::core::BnModule,
                    public VendorParameterMock {
   public:
+    ModuleMock() = default;
+    explicit ModuleMock(const Configuration& config) : mConfig(config) {}
     bool isScreenTurnedOn() const { return mIsScreenTurnedOn; }
     ScreenRotation getScreenRotation() const { return mScreenRotation; }
 
@@ -91,35 +207,91 @@
         return ndk::ScopedAStatus::ok();
     }
     ndk::ScopedAStatus connectExternalDevice(
-            const ::aidl::android::media::audio::common::AudioPort&,
-            ::aidl::android::media::audio::common::AudioPort*) override {
+            const ::aidl::android::media::audio::common::AudioPort& portIdAndData,
+            ::aidl::android::media::audio::common::AudioPort* port) override {
+        auto src = portIdAndData;  // Make a copy to mimic RPC behavior.
+        auto iter = findById<AudioPort>(mConfig.ports, src.id);
+        if (iter == mConfig.ports.end()) {
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        *port = *iter;
+        port->ext = src.ext;
+        port->id = mConfig.nextPortId++;
+        ALOGD("%s: returning %s", __func__, port->toString().c_str());
+        mConfig.ports.push_back(*port);
+        std::vector<AudioRoute> newRoutes;
+        for (auto& r : mConfig.routes) {
+            if (r.sinkPortId == src.id) {
+                newRoutes.push_back(AudioRoute{.sourcePortIds = r.sourcePortIds,
+                                               .sinkPortId = port->id,
+                                               .isExclusive = r.isExclusive});
+            } else if (std::find(r.sourcePortIds.begin(), r.sourcePortIds.end(), src.id) !=
+                       r.sourcePortIds.end()) {
+                r.sourcePortIds.push_back(port->id);
+            }
+        }
+        mConfig.routes.insert(mConfig.routes.end(), newRoutes.begin(), newRoutes.end());
         return ndk::ScopedAStatus::ok();
     }
-    ndk::ScopedAStatus disconnectExternalDevice(int32_t) override {
+    ndk::ScopedAStatus disconnectExternalDevice(int32_t portId) override {
+        auto iter = findById<AudioPort>(mConfig.ports, portId);
+        if (iter == mConfig.ports.end()) {
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        mConfig.ports.erase(iter);
+        for (auto it = mConfig.routes.begin(); it != mConfig.routes.end();) {
+            if (it->sinkPortId == portId) {
+                it = mConfig.routes.erase(it);
+            } else {
+                if (auto srcIt =
+                            std::find(it->sourcePortIds.begin(), it->sourcePortIds.end(), portId);
+                    srcIt != it->sourcePortIds.end()) {
+                    it->sourcePortIds.erase(srcIt);
+                }
+                ++it;
+            }
+        }
         return ndk::ScopedAStatus::ok();
     }
     ndk::ScopedAStatus getAudioPatches(
-            std::vector<::aidl::android::hardware::audio::core::AudioPatch>*) override {
+            std::vector<::aidl::android::hardware::audio::core::AudioPatch>* patches) override {
+        *patches = mConfig.patches;
         return ndk::ScopedAStatus::ok();
     }
-    ndk::ScopedAStatus getAudioPort(int32_t,
-                                    ::aidl::android::media::audio::common::AudioPort*) override {
+    ndk::ScopedAStatus getAudioPort(
+            int32_t portId, ::aidl::android::media::audio::common::AudioPort* port) override {
+        auto iter = findById<AudioPort>(mConfig.ports, portId);
+        if (iter == mConfig.ports.end()) {
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        *port = *iter;
         return ndk::ScopedAStatus::ok();
     }
     ndk::ScopedAStatus getAudioPortConfigs(
-            std::vector<::aidl::android::media::audio::common::AudioPortConfig>*) override {
+            std::vector<::aidl::android::media::audio::common::AudioPortConfig>* configs) override {
+        *configs = mConfig.portConfigs;
         return ndk::ScopedAStatus::ok();
     }
     ndk::ScopedAStatus getAudioPorts(
-            std::vector<::aidl::android::media::audio::common::AudioPort>*) override {
+            std::vector<::aidl::android::media::audio::common::AudioPort>* ports) override {
+        *ports = mConfig.ports;
         return ndk::ScopedAStatus::ok();
     }
     ndk::ScopedAStatus getAudioRoutes(
-            std::vector<::aidl::android::hardware::audio::core::AudioRoute>*) override {
+            std::vector<::aidl::android::hardware::audio::core::AudioRoute>* routes) override {
+        *routes = mConfig.routes;
         return ndk::ScopedAStatus::ok();
     }
     ndk::ScopedAStatus getAudioRoutesForAudioPort(
-            int32_t, std::vector<::aidl::android::hardware::audio::core::AudioRoute>*) override {
+            int32_t portId,
+            std::vector<::aidl::android::hardware::audio::core::AudioRoute>* routes) override {
+        for (auto& r : mConfig.routes) {
+            const auto& srcs = r.sourcePortIds;
+            if (r.sinkPortId == portId ||
+                std::find(srcs.begin(), srcs.end(), portId) != srcs.end()) {
+                routes->push_back(r);
+            }
+        }
         return ndk::ScopedAStatus::ok();
     }
     ndk::ScopedAStatus openInputStream(const OpenInputStreamArguments&,
@@ -133,17 +305,69 @@
     ndk::ScopedAStatus getSupportedPlaybackRateFactors(SupportedPlaybackRateFactors*) override {
         return ndk::ScopedAStatus::ok();
     }
-    ndk::ScopedAStatus setAudioPatch(const ::aidl::android::hardware::audio::core::AudioPatch&,
-                                     ::aidl::android::hardware::audio::core::AudioPatch*) override {
+    ndk::ScopedAStatus setAudioPatch(
+            const ::aidl::android::hardware::audio::core::AudioPatch& requested,
+            ::aidl::android::hardware::audio::core::AudioPatch* patch) override {
+        if (requested.id == 0) {
+            *patch = requested;
+            patch->id = mConfig.nextPatchId++;
+            mConfig.patches.push_back(*patch);
+            ALOGD("%s: returning %s", __func__, patch->toString().c_str());
+        } else {
+            auto iter = findById<AudioPatch>(mConfig.patches, requested.id);
+            if (iter == mConfig.patches.end()) {
+                return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+            }
+            *iter = *patch = requested;
+            ALOGD("%s: updated %s", __func__, patch->toString().c_str());
+        }
         return ndk::ScopedAStatus::ok();
     }
     ndk::ScopedAStatus setAudioPortConfig(
-            const ::aidl::android::media::audio::common::AudioPortConfig&,
-            ::aidl::android::media::audio::common::AudioPortConfig*, bool*) override {
+            const ::aidl::android::media::audio::common::AudioPortConfig& requested,
+            ::aidl::android::media::audio::common::AudioPortConfig* config,
+            bool* applied) override {
+        *applied = false;
+        auto src = requested;  // Make a copy to mimic RPC behavior.
+        if (src.id == 0) {
+            *config = src;
+            if (config->ext.getTag() == AudioPortExt::unspecified) {
+                auto iter = findById<AudioPort>(mConfig.ports, src.portId);
+                if (iter == mConfig.ports.end()) {
+                    return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+                }
+                config->ext = iter->ext;
+            }
+            config->id = mConfig.nextPortId++;
+            mConfig.portConfigs.push_back(*config);
+            ALOGD("%s: returning %s", __func__, config->toString().c_str());
+        } else {
+            auto iter = findById<AudioPortConfig>(mConfig.portConfigs, src.id);
+            if (iter == mConfig.portConfigs.end()) {
+                return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+            }
+            *iter = *config = src;
+            ALOGD("%s: updated %s", __func__, config->toString().c_str());
+        }
+        *applied = true;
         return ndk::ScopedAStatus::ok();
     }
-    ndk::ScopedAStatus resetAudioPatch(int32_t) override { return ndk::ScopedAStatus::ok(); }
-    ndk::ScopedAStatus resetAudioPortConfig(int32_t) override { return ndk::ScopedAStatus::ok(); }
+    ndk::ScopedAStatus resetAudioPatch(int32_t patchId) override {
+        auto iter = findById<AudioPatch>(mConfig.patches, patchId);
+        if (iter == mConfig.patches.end()) {
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        mConfig.patches.erase(iter);
+        return ndk::ScopedAStatus::ok();
+    }
+    ndk::ScopedAStatus resetAudioPortConfig(int32_t portConfigId) override {
+        auto iter = findById<AudioPortConfig>(mConfig.portConfigs, portConfigId);
+        if (iter == mConfig.portConfigs.end()) {
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+        }
+        mConfig.portConfigs.erase(iter);
+        return ndk::ScopedAStatus::ok();
+    }
     ndk::ScopedAStatus getMasterMute(bool*) override { return ndk::ScopedAStatus::ok(); }
     ndk::ScopedAStatus setMasterMute(bool) override { return ndk::ScopedAStatus::ok(); }
     ndk::ScopedAStatus getMasterVolume(float*) override { return ndk::ScopedAStatus::ok(); }
@@ -205,6 +429,7 @@
         return ndk::ScopedAStatus::ok();
     }
 
+    Configuration mConfig;
     bool mIsScreenTurnedOn = false;
     ScreenRotation mScreenRotation = ScreenRotation::DEG_0;
 };
@@ -398,6 +623,35 @@
 
 using namespace android;
 
+namespace {
+
+class StreamHalMock : public virtual StreamHalInterface {
+  public:
+    StreamHalMock() = default;
+    ~StreamHalMock() override = default;
+    status_t getBufferSize(size_t*) override { return OK; }
+    status_t getAudioProperties(audio_config_base_t*) override { return OK; }
+    status_t setParameters(const String8&) override { return OK; }
+    status_t getParameters(const String8&, String8*) override { return OK; }
+    status_t getFrameSize(size_t*) override { return OK; }
+    status_t addEffect(sp<EffectHalInterface>) override { return OK; }
+    status_t removeEffect(sp<EffectHalInterface>) override { return OK; }
+    status_t standby() override { return OK; }
+    status_t dump(int, const Vector<String16>&) override { return OK; }
+    status_t start() override { return OK; }
+    status_t stop() override { return OK; }
+    status_t createMmapBuffer(int32_t, struct audio_mmap_buffer_info*) override { return OK; }
+    status_t getMmapPosition(struct audio_mmap_position*) override { return OK; }
+    status_t setHalThreadPriority(int) override { return OK; }
+    status_t legacyCreateAudioPatch(const struct audio_port_config&, std::optional<audio_source_t>,
+                                    audio_devices_t) override {
+        return OK;
+    }
+    status_t legacyReleaseAudioPatch() override { return OK; }
+};
+
+}  // namespace
+
 class DeviceHalAidlTest : public testing::Test {
   public:
     void SetUp() override {
@@ -593,3 +847,297 @@
     EXPECT_EQ(0UL, mStreamCommon->getAsyncParameters().size());
     EXPECT_EQ(0UL, mStreamCommon->getSyncParameters().size());
 }
+
+class Hal2AidlMapperTest : public testing::Test {
+  public:
+    void SetUp() override {
+        mModule = ndk::SharedRefBase::make<ModuleMock>(getTestConfiguration());
+        mMapper = std::make_unique<Hal2AidlMapper>("test", mModule);
+        ASSERT_EQ(OK, mMapper->initialize());
+
+        mConnectedPort.ext = createPortDeviceExt(AudioDeviceType::OUT_DEVICE, 0,
+                                                 AudioDeviceDescription::CONNECTION_BT_A2DP);
+        mConnectedPort.ext.get<AudioPortExt::device>().device.address = "00:11:22:33:44:55";
+        ASSERT_EQ(OK, mMapper->setDevicePortConnectedState(mConnectedPort, true /*connected*/));
+
+        std::mutex mutex;  // Only needed for cleanups.
+        auto mapperAccessor = std::make_unique<LockedAccessor<Hal2AidlMapper>>(*mMapper, mutex);
+        Hal2AidlMapper::Cleanups cleanups(*mapperAccessor);
+        AudioConfig config;
+        config.base.channelMask = AudioChannelLayout::make<AudioChannelLayout::layoutMask>(
+                AudioChannelLayout::LAYOUT_STEREO);
+        config.base.format =
+                AudioFormatDescription{.type = AudioFormatType::PCM, .pcm = PcmType::INT_16_BIT};
+        config.base.sampleRate = 48000;
+        ASSERT_EQ(OK,
+                  mMapper->prepareToOpenStream(
+                          42 /*ioHandle*/, mConnectedPort.ext.get<AudioPortExt::device>().device,
+                          AudioIoFlags::make<AudioIoFlags::output>(0), AudioSource::DEFAULT,
+                          &cleanups, &config, &mMixPortConfig, &mPatch));
+        cleanups.disarmAll();
+        ASSERT_NE(0, mPatch.id);
+        ASSERT_NE(0, mMixPortConfig.id);
+        mStream = sp<StreamHalMock>::make();
+        mMapper->addStream(mStream, mMixPortConfig.id, mPatch.id);
+
+        ASSERT_EQ(OK, mMapper->findPortConfig(mConnectedPort.ext.get<AudioPortExt::device>().device,
+                                              &mDevicePortConfig));
+        ASSERT_EQ(1UL, mPatch.sourcePortConfigIds.size());
+        ASSERT_EQ(mMixPortConfig.id, mPatch.sourcePortConfigIds[0]);
+        ASSERT_EQ(1UL, mPatch.sinkPortConfigIds.size());
+        ASSERT_EQ(mDevicePortConfig.id, mPatch.sinkPortConfigIds[0]);
+    }
+
+    void TearDown() override {
+        mStream.clear();
+        mMapper.reset();
+        mModule.reset();
+    }
+
+  protected:
+    void CloseDisconnectImpl() {
+        mStream.clear();
+        ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    }
+
+    void ConnectAnotherDevice() {
+        mConnectedPort.ext.get<AudioPortExt::device>().device.address = "00:11:22:33:44:66";
+        ASSERT_EQ(OK, mMapper->setDevicePortConnectedState(mConnectedPort, true /*connected*/));
+    }
+
+    void CreateFwkPatch(int32_t* patchId) {
+        std::mutex mutex;  // Only needed for cleanups.
+        auto mapperAccessor = std::make_unique<LockedAccessor<Hal2AidlMapper>>(*mMapper, mutex);
+        Hal2AidlMapper::Cleanups cleanups(*mapperAccessor);
+        ASSERT_EQ(OK, mMapper->createOrUpdatePatch({mMixPortConfig}, {mDevicePortConfig}, patchId,
+                                                   &cleanups));
+        cleanups.disarmAll();
+    }
+
+    void DisconnectDevice() {
+        ASSERT_EQ(OK, mMapper->prepareToDisconnectExternalDevice(mConnectedPort));
+        ASSERT_EQ(OK, mMapper->setDevicePortConnectedState(mConnectedPort, false /*connected*/));
+    }
+
+    void ReleaseFwkOnlyPatch(int32_t patchId) {
+        // The patch only exists for the framework.
+        EXPECT_EQ(patchId, mMapper->findFwkPatch(patchId));
+        ASSERT_EQ(BAD_VALUE, mMapper->releaseAudioPatch(patchId));
+        mMapper->eraseFwkPatch(patchId);
+        // The patch is now erased.
+        EXPECT_EQ(0, mMapper->findFwkPatch(patchId));
+    }
+
+    std::shared_ptr<ModuleMock> mModule;
+    std::unique_ptr<Hal2AidlMapper> mMapper;
+    AudioPort mConnectedPort;
+    AudioPortConfig mMixPortConfig;
+    AudioPortConfig mDevicePortConfig;
+    AudioPatch mPatch;
+    sp<StreamHalInterface> mStream;
+};
+
+/**
+ * External device connections and patches tests diagram.
+ *
+ * [Connect device] -> [Create Stream]
+ *                            |-> [ (1) Close Stream] -> [Disconnect Device]
+ *                            |-> [ (2) Disconnect Device]
+ *                            |          |-> [ (3) Close Stream]
+ *                            |          \-> [ (4) Connect Another Device]
+ *                            |                    |-> (1)
+ *                            |                    |-> (2) -> (3)
+ *                            |                    \-> (5) -> (7)
+ *                            \-> [ (5) Create/Update Fwk Patch]
+ *                                       |-> [(6) Release Fwk Patch]
+ *                                       |        |-> (1)
+ *                                       |        \-> (2) (including reconnection)
+ *                                       \-> [(7) Disconnect Device]
+ *                                                |-> [Release Fwk Patch] -> [Close Stream]
+ *                                                \-> (4) -> (5) -> (6) -> (1)
+ *
+ * Note that the test (acting on behalf of DeviceHalAidl) is responsible
+ * for calling `eraseFwkPatch` and `updateFwkPatch` when needed.
+ */
+
+// (1)
+TEST_F(Hal2AidlMapperTest, CloseDisconnect) {
+    ASSERT_NO_FATAL_FAILURE(CloseDisconnectImpl());
+    // The patch is owned by HAL, must not be listed under fwkPatches after disconnection.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+}
+
+// (2) -> (3)
+TEST_F(Hal2AidlMapperTest, DisconnectClose) {
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch is owned by HAL, must not be listed under fwkPatches after disconnection.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+    mStream.clear();
+}
+
+// (2) -> (4) -> (1)
+TEST_F(Hal2AidlMapperTest, DisconnectConnectCloseDisconnect) {
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch is owned by HAL, must not be listed under fwkPatches after disconnection.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+    ASSERT_NO_FATAL_FAILURE(ConnectAnotherDevice());
+    ASSERT_NO_FATAL_FAILURE(CloseDisconnectImpl());
+    // The patch is owned by HAL, must not be listed under fwkPatches after disconnection.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+}
+
+// (2) -> (4) -> (2) -> (3)
+TEST_F(Hal2AidlMapperTest, DisconnectConnectDisconnectClose) {
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch is owned by HAL, must not be listed under fwkPatches after disconnection.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+    ASSERT_NO_FATAL_FAILURE(ConnectAnotherDevice());
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch is owned by HAL, must not be listed under fwkPatches after disconnection.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+    mStream.clear();
+}
+
+// (5) -> (6) -> (1)
+TEST_F(Hal2AidlMapperTest, CreateFwkPatchReleaseCloseDisconnect) {
+    int32_t patchId;
+    ASSERT_NO_FATAL_FAILURE(CreateFwkPatch(&patchId));
+    // Must be the patch created during stream opening.
+    ASSERT_EQ(mPatch.id, patchId);
+    // The patch was not reset by HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+
+    ASSERT_EQ(OK, mMapper->releaseAudioPatch(patchId));
+    // The patch does not exist both for the fwk and the HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(patchId));
+    ASSERT_NO_FATAL_FAILURE(CloseDisconnectImpl());
+    // The patch does not exist both for the fwk and the HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(patchId));
+}
+
+// (5) -> (6) -> (2) -> (3)
+TEST_F(Hal2AidlMapperTest, CreateFwkPatchReleaseDisconnectClose) {
+    int32_t patchId;
+    ASSERT_NO_FATAL_FAILURE(CreateFwkPatch(&patchId));
+    // Must be the patch created during stream opening.
+    ASSERT_EQ(mPatch.id, patchId);
+    // The patch was not reset by HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+
+    ASSERT_EQ(OK, mMapper->releaseAudioPatch(patchId));
+    // The patch does not exist both for the fwk and the HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(patchId));
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch does not exist both for the fwk and the HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+    mStream.clear();
+}
+
+// (5) -> (6) -> (2) -> (4) -> (2) -> (3)
+TEST_F(Hal2AidlMapperTest, CreateFwkPatchReleaseDisconnectConnectDisconnectClose) {
+    int32_t patchId;
+    ASSERT_NO_FATAL_FAILURE(CreateFwkPatch(&patchId));
+    // Must be the patch created during stream opening.
+    ASSERT_EQ(mPatch.id, patchId);
+    // The patch was not reset by HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+
+    ASSERT_EQ(OK, mMapper->releaseAudioPatch(patchId));
+    // The patch does not exist both for the fwk and the HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(patchId));
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch does not exist both for the fwk and the HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+
+    ASSERT_NO_FATAL_FAILURE(ConnectAnotherDevice());
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch does not exist both for the fwk and the HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+    mStream.clear();
+}
+
+// (5) -> (7) -> Release -> Close
+TEST_F(Hal2AidlMapperTest, CreateFwkPatchDisconnectReleaseClose) {
+    int32_t patchId;
+    ASSERT_NO_FATAL_FAILURE(CreateFwkPatch(&patchId));
+    // Must be the patch created during stream opening.
+    ASSERT_EQ(mPatch.id, patchId);
+    // The patch was not reset by HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    ASSERT_NO_FATAL_FAILURE(ReleaseFwkOnlyPatch(patchId));
+
+    mStream.clear();
+    EXPECT_EQ(0, mMapper->findFwkPatch(patchId));
+}
+
+// (5) -> (7) -> (4) -> (5) -> (6) -> (1)
+TEST_F(Hal2AidlMapperTest, CreateFwkPatchDisconnectConnectUpdateReleaseCloseDisconnect) {
+    int32_t patchId;
+    ASSERT_NO_FATAL_FAILURE(CreateFwkPatch(&patchId));
+    // Must be the patch created during stream opening.
+    ASSERT_EQ(mPatch.id, patchId);
+    // The patch was not reset by HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch now only exists for the framework.
+    EXPECT_EQ(mPatch.id, mMapper->findFwkPatch(mPatch.id));
+
+    ASSERT_NO_FATAL_FAILURE(ConnectAnotherDevice());
+    // Change the device address locally, for patch update.
+    mDevicePortConfig.ext.get<AudioPortExt::device>().device.address =
+            mConnectedPort.ext.get<AudioPortExt::device>().device.address;
+    int32_t newPatchId = patchId;
+    ASSERT_NO_FATAL_FAILURE(CreateFwkPatch(&newPatchId));
+    EXPECT_NE(patchId, newPatchId);
+    mMapper->updateFwkPatch(patchId, newPatchId);
+    EXPECT_EQ(newPatchId, mMapper->findFwkPatch(patchId));
+    // Just in case, check that HAL patch ID is not listed as a fwk patch.
+    EXPECT_EQ(0, mMapper->findFwkPatch(newPatchId));
+    // Verify that device port config was updated.
+    ASSERT_EQ(OK, mMapper->findPortConfig(mConnectedPort.ext.get<AudioPortExt::device>().device,
+                                          &mDevicePortConfig));
+
+    ASSERT_EQ(OK, mMapper->releaseAudioPatch(newPatchId));
+    // The patch does not exist both for the fwk and the HAL, must not be listed under fwkPatches.
+    EXPECT_EQ(0, mMapper->findFwkPatch(patchId));
+    // Just in case, check that HAL patch ID is not listed.
+    EXPECT_EQ(0, mMapper->findFwkPatch(newPatchId));
+
+    ASSERT_NO_FATAL_FAILURE(CloseDisconnectImpl());
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+    EXPECT_EQ(0, mMapper->findFwkPatch(patchId));
+    EXPECT_EQ(0, mMapper->findFwkPatch(newPatchId));
+}
+
+// (2) -> (4) -> (5) -> (7) -> Release -> Close
+TEST_F(Hal2AidlMapperTest, DisconnectConnectCreateFwkPatchDisconnectReleaseClose) {
+    const int32_t patchId = mPatch.id;
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    // The patch is owned by HAL, must not be listed under fwkPatches after disconnection.
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+
+    ASSERT_NO_FATAL_FAILURE(ConnectAnotherDevice());
+    // Change the device address locally, for patch update.
+    mDevicePortConfig.ext.get<AudioPortExt::device>().device.address =
+            mConnectedPort.ext.get<AudioPortExt::device>().device.address;
+    int32_t newPatchId = 0;  // Use 0 since the fwk does not know about the HAL patch.
+    EXPECT_EQ(0, mMapper->findFwkPatch(newPatchId));
+    ASSERT_NO_FATAL_FAILURE(CreateFwkPatch(&newPatchId));
+    EXPECT_NE(0, newPatchId);
+    EXPECT_NE(patchId, newPatchId);
+    // Just in case, check that HAL patch ID is not listed as a fwk patch.
+    EXPECT_EQ(0, mMapper->findFwkPatch(newPatchId));
+    // Verify that device port config was updated.
+    ASSERT_EQ(OK, mMapper->findPortConfig(mConnectedPort.ext.get<AudioPortExt::device>().device,
+                                          &mDevicePortConfig));
+
+    ASSERT_NO_FATAL_FAILURE(DisconnectDevice());
+    ASSERT_NO_FATAL_FAILURE(ReleaseFwkOnlyPatch(newPatchId));
+
+    mStream.clear();
+    EXPECT_EQ(0, mMapper->findFwkPatch(mPatch.id));
+    EXPECT_EQ(0, mMapper->findFwkPatch(newPatchId));
+}