Merge "Updated face session to use SurfaceView" into main
diff --git a/audio/aidl/common/include/Utils.h b/audio/aidl/common/include/Utils.h
index 3b08de7..59ca92a 100644
--- a/audio/aidl/common/include/Utils.h
+++ b/audio/aidl/common/include/Utils.h
@@ -174,4 +174,12 @@
     return result;
 }
 
+constexpr int32_t frameCountFromDurationUs(long durationUs, int32_t sampleRateHz) {
+    return (durationUs * sampleRateHz) / 1000000LL;
+}
+
+constexpr int32_t frameCountFromDurationMs(int32_t durationMs, int32_t sampleRateHz) {
+    return frameCountFromDurationUs(durationMs * 1000, sampleRateHz);
+}
+
 }  // namespace aidl::android::hardware::audio::common
diff --git a/audio/aidl/default/Android.bp b/audio/aidl/default/Android.bp
index 8710758..11bd7d3 100644
--- a/audio/aidl/default/Android.bp
+++ b/audio/aidl/default/Android.bp
@@ -96,7 +96,6 @@
     shared_libs: [
         "libaudio_aidl_conversion_common_ndk",
         "libaudioutils",
-        "libaudioutils_nonvndk",
         "libbluetooth_audio_session_aidl",
         "liblog",
         "libmedia_helper",
@@ -131,7 +130,6 @@
         "libaudioserviceexampleimpl",
     ],
     shared_libs: [
-        "libaudioutils_nonvndk",
         "libaudio_aidl_conversion_common_ndk",
         "libbluetooth_audio_session_aidl",
         "liblog",
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index 1ec171a..66c2169 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -18,7 +18,6 @@
 #include <set>
 
 #define LOG_TAG "AHAL_Module"
-#include <Utils.h>
 #include <aidl/android/media/audio/common/AudioInputFlags.h>
 #include <aidl/android/media/audio/common/AudioOutputFlags.h>
 #include <android-base/logging.h>
@@ -35,6 +34,7 @@
 #include "core-impl/SoundDose.h"
 #include "core-impl/utils.h"
 
+using aidl::android::hardware::audio::common::frameCountFromDurationMs;
 using aidl::android::hardware::audio::common::getFrameSizeInBytes;
 using aidl::android::hardware::audio::common::isBitPositionFlagSet;
 using aidl::android::hardware::audio::common::isValidAudioMode;
@@ -202,15 +202,17 @@
         LOG(ERROR) << __func__ << ": non-positive buffer size " << in_bufferSizeFrames;
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
     }
-    if (in_bufferSizeFrames < kMinimumStreamBufferSizeFrames) {
-        LOG(ERROR) << __func__ << ": insufficient buffer size " << in_bufferSizeFrames
-                   << ", must be at least " << kMinimumStreamBufferSizeFrames;
-        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
-    }
     auto& configs = getConfig().portConfigs;
     auto portConfigIt = findById<AudioPortConfig>(configs, in_portConfigId);
     // Since this is a private method, it is assumed that
     // validity of the portConfigId has already been checked.
+    const int32_t minimumStreamBufferSizeFrames = calculateBufferSizeFrames(
+            getNominalLatencyMs(*portConfigIt), portConfigIt->sampleRate.value().value);
+    if (in_bufferSizeFrames < minimumStreamBufferSizeFrames) {
+        LOG(ERROR) << __func__ << ": insufficient buffer size " << in_bufferSizeFrames
+                   << ", must be at least " << minimumStreamBufferSizeFrames;
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
     const size_t frameSize =
             getFrameSizeInBytes(portConfigIt->format.value(), portConfigIt->channelMask.value());
     if (frameSize == 0) {
@@ -243,8 +245,8 @@
         StreamContext temp(
                 std::make_unique<StreamContext::CommandMQ>(1, true /*configureEventFlagWord*/),
                 std::make_unique<StreamContext::ReplyMQ>(1, true /*configureEventFlagWord*/),
-                portConfigIt->portId, portConfigIt->format.value(),
-                portConfigIt->channelMask.value(), portConfigIt->sampleRate.value().value, flags,
+                portConfigIt->format.value(), portConfigIt->channelMask.value(),
+                portConfigIt->sampleRate.value().value, flags, getNominalLatencyMs(*portConfigIt),
                 portConfigIt->ext.get<AudioPortExt::mix>().handle,
                 std::make_unique<StreamContext::DataMQ>(frameSize * in_bufferSizeFrames),
                 asyncCallback, outEventCallback, mSoundDose.getInstance(), params);
@@ -363,6 +365,12 @@
     return internal::getConfiguration(getType());
 }
 
+int32_t Module::getNominalLatencyMs(const AudioPortConfig&) {
+    // Arbitrary value. Implementations must override this method to provide their actual latency.
+    static constexpr int32_t kLatencyMs = 5;
+    return kLatencyMs;
+}
+
 std::vector<AudioRoute*> Module::getAudioRoutesForAudioPortImpl(int32_t portId) {
     std::vector<AudioRoute*> result;
     auto& routes = getConfig().routes;
@@ -613,32 +621,30 @@
 
     std::vector<AudioRoute*> routesToMixPorts = getAudioRoutesForAudioPortImpl(templateId);
     std::set<int32_t> routableMixPortIds = getRoutableAudioPortIds(templateId, &routesToMixPorts);
-    if (hasDynamicProfilesOnly(connectedPort.profiles)) {
-        if (!mDebug.simulateDeviceConnections) {
-            RETURN_STATUS_IF_ERROR(populateConnectedDevicePort(&connectedPort));
-        } else {
-            auto& connectedProfiles = getConfig().connectedProfiles;
-            if (auto connectedProfilesIt = connectedProfiles.find(templateId);
-                connectedProfilesIt != connectedProfiles.end()) {
-                connectedPort.profiles = connectedProfilesIt->second;
-            }
+    if (!mDebug.simulateDeviceConnections) {
+        // Even if the device port has static profiles, the HAL module might need to update
+        // them, or abort the connection process.
+        RETURN_STATUS_IF_ERROR(populateConnectedDevicePort(&connectedPort));
+    } else if (hasDynamicProfilesOnly(connectedPort.profiles)) {
+        auto& connectedProfiles = getConfig().connectedProfiles;
+        if (auto connectedProfilesIt = connectedProfiles.find(templateId);
+            connectedProfilesIt != connectedProfiles.end()) {
+            connectedPort.profiles = connectedProfilesIt->second;
         }
-        if (hasDynamicProfilesOnly(connectedPort.profiles)) {
-            // Possible case 2. Check if all routable mix ports have static profiles.
-            if (auto dynamicMixPortIt = std::find_if(ports.begin(), ports.end(),
-                                                     [&routableMixPortIds](const auto& p) {
-                                                         return routableMixPortIds.count(p.id) >
-                                                                        0 &&
-                                                                hasDynamicProfilesOnly(p.profiles);
-                                                     });
-                dynamicMixPortIt != ports.end()) {
-                LOG(ERROR) << __func__
-                           << ": connected port only has dynamic profiles after connecting "
-                           << "external device " << connectedPort.toString() << ", and there exist "
-                           << "a routable mix port with dynamic profiles: "
-                           << dynamicMixPortIt->toString();
-                return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
-            }
+    }
+    if (hasDynamicProfilesOnly(connectedPort.profiles)) {
+        // Possible case 2. Check if all routable mix ports have static profiles.
+        if (auto dynamicMixPortIt = std::find_if(ports.begin(), ports.end(),
+                                                 [&routableMixPortIds](const auto& p) {
+                                                     return routableMixPortIds.count(p.id) > 0 &&
+                                                            hasDynamicProfilesOnly(p.profiles);
+                                                 });
+            dynamicMixPortIt != ports.end()) {
+            LOG(ERROR) << __func__ << ": connected port only has dynamic profiles after connecting "
+                       << "external device " << connectedPort.toString() << ", and there exist "
+                       << "a routable mix port with dynamic profiles: "
+                       << dynamicMixPortIt->toString();
+            return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
         }
     }
 
@@ -969,11 +975,21 @@
             return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
         }
     }
+    // Find the highest sample rate among mix port configs.
+    std::map<int32_t, AudioPortConfig*> sampleRates;
+    std::vector<AudioPortConfig*>& mixPortConfigs =
+            sources[0]->ext.getTag() == AudioPortExt::mix ? sources : sinks;
+    for (auto mix : mixPortConfigs) {
+        sampleRates.emplace(mix->sampleRate.value().value, mix);
+    }
     *_aidl_return = in_requested;
-    _aidl_return->minimumStreamBufferSizeFrames = kMinimumStreamBufferSizeFrames;
+    auto maxSampleRateIt = std::max_element(sampleRates.begin(), sampleRates.end());
+    const int32_t latencyMs = getNominalLatencyMs(*(maxSampleRateIt->second));
+    _aidl_return->minimumStreamBufferSizeFrames =
+            calculateBufferSizeFrames(latencyMs, maxSampleRateIt->first);
     _aidl_return->latenciesMs.clear();
     _aidl_return->latenciesMs.insert(_aidl_return->latenciesMs.end(),
-                                     _aidl_return->sinkPortConfigIds.size(), kLatencyMs);
+                                     _aidl_return->sinkPortConfigIds.size(), latencyMs);
     AudioPatch oldPatch{};
     if (existing == patches.end()) {
         _aidl_return->id = getConfig().nextPatchId++;
@@ -1215,7 +1231,7 @@
         // Reset master mute if it failed.
         onMasterMuteChanged(mMasterMute);
     }
-    return std::move(result);
+    return result;
 }
 
 ndk::ScopedAStatus Module::getMasterVolume(float* _aidl_return) {
@@ -1237,7 +1253,7 @@
                        << "), error=" << result;
             onMasterVolumeChanged(mMasterVolume);
         }
-        return std::move(result);
+        return result;
     }
     LOG(ERROR) << __func__ << ": invalid master volume value: " << in_volume;
     return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
@@ -1558,11 +1574,6 @@
     return result;
 }
 
-Module::BtProfileHandles Module::getBtProfileManagerHandles() {
-    return std::make_tuple(std::weak_ptr<IBluetooth>(), std::weak_ptr<IBluetoothA2dp>(),
-                           std::weak_ptr<IBluetoothLe>());
-}
-
 ndk::ScopedAStatus Module::bluetoothParametersUpdated() {
     return mStreams.bluetoothParametersUpdated();
 }
diff --git a/audio/aidl/default/ModulePrimary.cpp b/audio/aidl/default/ModulePrimary.cpp
index 9919c7f..3da6d48 100644
--- a/audio/aidl/default/ModulePrimary.cpp
+++ b/audio/aidl/default/ModulePrimary.cpp
@@ -58,4 +58,12 @@
                                                   offloadInfo);
 }
 
+int32_t ModulePrimary::getNominalLatencyMs(const AudioPortConfig&) {
+    // 85 ms is chosen considering 4096 frames @ 48 kHz. This is the value which allows
+    // the virtual Android device implementation to pass CTS. Hardware implementations
+    // should have significantly lower latency.
+    static constexpr int32_t kLatencyMs = 85;
+    return kLatencyMs;
+}
+
 }  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/Stream.cpp b/audio/aidl/default/Stream.cpp
index f00e358..e8c1693 100644
--- a/audio/aidl/default/Stream.cpp
+++ b/audio/aidl/default/Stream.cpp
@@ -138,7 +138,7 @@
     reply->status = STATUS_OK;
     if (isConnected) {
         reply->observable.frames = mContext->getFrameCount();
-        reply->observable.timeNs = ::android::elapsedRealtimeNano();
+        reply->observable.timeNs = ::android::uptimeNanos();
         if (auto status = mDriver->refinePosition(&reply->observable); status == ::android::OK) {
             return;
         }
@@ -315,7 +315,7 @@
     const size_t frameSize = mContext->getFrameSize();
     size_t actualFrameCount = 0;
     bool fatal = false;
-    int32_t latency = Module::kLatencyMs;
+    int32_t latency = mContext->getNominalLatencyMs();
     if (isConnected) {
         if (::android::status_t status = mDriver->transfer(mDataBuffer.get(), byteCount / frameSize,
                                                            &actualFrameCount, &latency);
@@ -581,7 +581,7 @@
     const size_t readByteCount = dataMQ->availableToRead();
     const size_t frameSize = mContext->getFrameSize();
     bool fatal = false;
-    int32_t latency = Module::kLatencyMs;
+    int32_t latency = mContext->getNominalLatencyMs();
     if (bool success = readByteCount > 0 ? dataMQ->read(&mDataBuffer[0], readByteCount) : true) {
         const bool isConnected = mIsConnected;
         LOG(VERBOSE) << __func__ << ": reading of " << readByteCount << " bytes from data MQ"
@@ -848,7 +848,7 @@
 }
 
 StreamInHwGainHelper::StreamInHwGainHelper(const StreamContext* context)
-    : mChannelCount(getChannelCount(context->getChannelLayout())) {}
+    : mChannelCount(getChannelCount(context->getChannelLayout())), mHwGains(mChannelCount, 0.0f) {}
 
 ndk::ScopedAStatus StreamInHwGainHelper::getHwGainImpl(std::vector<float>* _aidl_return) {
     *_aidl_return = mHwGains;
@@ -979,7 +979,8 @@
 }
 
 StreamOutHwVolumeHelper::StreamOutHwVolumeHelper(const StreamContext* context)
-    : mChannelCount(getChannelCount(context->getChannelLayout())) {}
+    : mChannelCount(getChannelCount(context->getChannelLayout())),
+      mHwVolumes(mChannelCount, 0.0f) {}
 
 ndk::ScopedAStatus StreamOutHwVolumeHelper::getHwVolumeImpl(std::vector<float>* _aidl_return) {
     *_aidl_return = mHwVolumes;
diff --git a/audio/aidl/default/alsa/Mixer.cpp b/audio/aidl/default/alsa/Mixer.cpp
index 126c033..e72502b 100644
--- a/audio/aidl/default/alsa/Mixer.cpp
+++ b/audio/aidl/default/alsa/Mixer.cpp
@@ -20,9 +20,24 @@
 #define LOG_TAG "AHAL_AlsaMixer"
 #include <android-base/logging.h>
 #include <android/binder_status.h>
+#include <error/expected_utils.h>
 
 #include "Mixer.h"
 
+namespace ndk {
+
+// This enables use of 'error/expected_utils' for ScopedAStatus.
+
+inline bool errorIsOk(const ScopedAStatus& s) {
+    return s.isOk();
+}
+
+inline std::string errorToString(const ScopedAStatus& s) {
+    return s.getDescription();
+}
+
+}  // namespace ndk
+
 namespace aidl::android::hardware::audio::core::alsa {
 
 // static
@@ -93,6 +108,36 @@
     }
 }
 
+ndk::ScopedAStatus Mixer::getMasterMute(bool* muted) {
+    return getMixerControlMute(MASTER_SWITCH, muted);
+}
+
+ndk::ScopedAStatus Mixer::getMasterVolume(float* volume) {
+    return getMixerControlVolume(MASTER_VOLUME, volume);
+}
+
+ndk::ScopedAStatus Mixer::getMicGain(float* gain) {
+    return getMixerControlVolume(MIC_GAIN, gain);
+}
+
+ndk::ScopedAStatus Mixer::getMicMute(bool* muted) {
+    return getMixerControlMute(MIC_SWITCH, muted);
+}
+
+ndk::ScopedAStatus Mixer::getVolumes(std::vector<float>* volumes) {
+    struct mixer_ctl* mctl;
+    RETURN_STATUS_IF_ERROR(findControl(Mixer::HW_VOLUME, &mctl));
+    std::vector<int> percents;
+    std::lock_guard l(mMixerAccess);
+    if (int err = getMixerControlPercent(mctl, &percents); err != 0) {
+        LOG(ERROR) << __func__ << ": failed to get volume, err=" << err;
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    std::transform(percents.begin(), percents.end(), std::back_inserter(*volumes),
+                   [](int percent) -> float { return std::clamp(percent / 100.0f, 0.0f, 1.0f); });
+    return ndk::ScopedAStatus::ok();
+}
+
 ndk::ScopedAStatus Mixer::setMasterMute(bool muted) {
     return setMixerControlMute(MASTER_SWITCH, muted);
 }
@@ -110,35 +155,70 @@
 }
 
 ndk::ScopedAStatus Mixer::setVolumes(const std::vector<float>& volumes) {
-    if (!isValid()) {
-        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
-    }
-    auto it = mMixerControls.find(Mixer::HW_VOLUME);
-    if (it == mMixerControls.end()) {
-        return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
-    }
+    struct mixer_ctl* mctl;
+    RETURN_STATUS_IF_ERROR(findControl(Mixer::HW_VOLUME, &mctl));
     std::vector<int> percents;
     std::transform(
             volumes.begin(), volumes.end(), std::back_inserter(percents),
             [](float volume) -> int { return std::floor(std::clamp(volume, 0.0f, 1.0f) * 100); });
     std::lock_guard l(mMixerAccess);
-    if (int err = setMixerControlPercent(it->second, percents); err != 0) {
+    if (int err = setMixerControlPercent(mctl, percents); err != 0) {
         LOG(ERROR) << __func__ << ": failed to set volume, err=" << err;
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     }
     return ndk::ScopedAStatus::ok();
 }
 
-ndk::ScopedAStatus Mixer::setMixerControlMute(Mixer::Control ctl, bool muted) {
+ndk::ScopedAStatus Mixer::findControl(Control ctl, struct mixer_ctl** result) {
     if (!isValid()) {
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     }
-    auto it = mMixerControls.find(ctl);
-    if (it == mMixerControls.end()) {
-        return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+    if (auto it = mMixerControls.find(ctl); it != mMixerControls.end()) {
+        *result = it->second;
+        return ndk::ScopedAStatus::ok();
     }
+    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+}
+
+ndk::ScopedAStatus Mixer::getMixerControlMute(Control ctl, bool* muted) {
+    struct mixer_ctl* mctl;
+    RETURN_STATUS_IF_ERROR(findControl(ctl, &mctl));
     std::lock_guard l(mMixerAccess);
-    if (int err = setMixerControlValue(it->second, muted ? 0 : 1); err != 0) {
+    std::vector<int> mutedValues;
+    if (int err = getMixerControlValues(mctl, &mutedValues); err != 0) {
+        LOG(ERROR) << __func__ << ": failed to get " << ctl << ", err=" << err;
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    if (mutedValues.empty()) {
+        LOG(ERROR) << __func__ << ": got no values for " << ctl;
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    *muted = mutedValues[0] != 0;
+    return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus Mixer::getMixerControlVolume(Control ctl, float* volume) {
+    struct mixer_ctl* mctl;
+    RETURN_STATUS_IF_ERROR(findControl(ctl, &mctl));
+    std::lock_guard l(mMixerAccess);
+    std::vector<int> percents;
+    if (int err = getMixerControlPercent(mctl, &percents); err != 0) {
+        LOG(ERROR) << __func__ << ": failed to get " << ctl << ", err=" << err;
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    if (percents.empty()) {
+        LOG(ERROR) << __func__ << ": got no values for " << ctl;
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    *volume = std::clamp(percents[0] / 100.0f, 0.0f, 1.0f);
+    return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus Mixer::setMixerControlMute(Control ctl, bool muted) {
+    struct mixer_ctl* mctl;
+    RETURN_STATUS_IF_ERROR(findControl(ctl, &mctl));
+    std::lock_guard l(mMixerAccess);
+    if (int err = setMixerControlValue(mctl, muted ? 0 : 1); err != 0) {
         LOG(ERROR) << __func__ << ": failed to set " << ctl << " to " << muted << ", err=" << err;
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     }
@@ -146,54 +226,72 @@
 }
 
 ndk::ScopedAStatus Mixer::setMixerControlVolume(Control ctl, float volume) {
-    if (!isValid()) {
-        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
-    }
-    auto it = mMixerControls.find(ctl);
-    if (it == mMixerControls.end()) {
-        return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
-    }
+    struct mixer_ctl* mctl;
+    RETURN_STATUS_IF_ERROR(findControl(ctl, &mctl));
     volume = std::clamp(volume, 0.0f, 1.0f);
     std::lock_guard l(mMixerAccess);
-    if (int err = setMixerControlPercent(it->second, std::floor(volume * 100)); err != 0) {
+    if (int err = setMixerControlPercent(mctl, std::floor(volume * 100)); err != 0) {
         LOG(ERROR) << __func__ << ": failed to set " << ctl << " to " << volume << ", err=" << err;
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     }
     return ndk::ScopedAStatus::ok();
 }
 
+int Mixer::getMixerControlPercent(struct mixer_ctl* ctl, std::vector<int>* percents) {
+    const unsigned int n = mixer_ctl_get_num_values(ctl);
+    percents->resize(n);
+    for (unsigned int id = 0; id < n; id++) {
+        if (int valueOrError = mixer_ctl_get_percent(ctl, id); valueOrError >= 0) {
+            (*percents)[id] = valueOrError;
+        } else {
+            return valueOrError;
+        }
+    }
+    return 0;
+}
+
+int Mixer::getMixerControlValues(struct mixer_ctl* ctl, std::vector<int>* values) {
+    const unsigned int n = mixer_ctl_get_num_values(ctl);
+    values->resize(n);
+    for (unsigned int id = 0; id < n; id++) {
+        if (int valueOrError = mixer_ctl_get_value(ctl, id); valueOrError >= 0) {
+            (*values)[id] = valueOrError;
+        } else {
+            return valueOrError;
+        }
+    }
+    return 0;
+}
+
 int Mixer::setMixerControlPercent(struct mixer_ctl* ctl, int percent) {
-    int ret = 0;
     const unsigned int n = mixer_ctl_get_num_values(ctl);
     for (unsigned int id = 0; id < n; id++) {
         if (int error = mixer_ctl_set_percent(ctl, id, percent); error != 0) {
-            ret = error;
+            return error;
         }
     }
-    return ret;
+    return 0;
 }
 
 int Mixer::setMixerControlPercent(struct mixer_ctl* ctl, const std::vector<int>& percents) {
-    int ret = 0;
     const unsigned int n = mixer_ctl_get_num_values(ctl);
     for (unsigned int id = 0; id < n; id++) {
         if (int error = mixer_ctl_set_percent(ctl, id, id < percents.size() ? percents[id] : 0);
             error != 0) {
-            ret = error;
+            return error;
         }
     }
-    return ret;
+    return 0;
 }
 
 int Mixer::setMixerControlValue(struct mixer_ctl* ctl, int value) {
-    int ret = 0;
     const unsigned int n = mixer_ctl_get_num_values(ctl);
     for (unsigned int id = 0; id < n; id++) {
         if (int error = mixer_ctl_set_value(ctl, id, value); error != 0) {
-            ret = error;
+            return error;
         }
     }
-    return ret;
+    return 0;
 }
 
 }  // namespace aidl::android::hardware::audio::core::alsa
diff --git a/audio/aidl/default/alsa/Mixer.h b/audio/aidl/default/alsa/Mixer.h
index 8fba1e0..41f19a8 100644
--- a/audio/aidl/default/alsa/Mixer.h
+++ b/audio/aidl/default/alsa/Mixer.h
@@ -39,6 +39,11 @@
 
     bool isValid() const { return mMixer != nullptr; }
 
+    ndk::ScopedAStatus getMasterMute(bool* muted);
+    ndk::ScopedAStatus getMasterVolume(float* volume);
+    ndk::ScopedAStatus getMicGain(float* gain);
+    ndk::ScopedAStatus getMicMute(bool* muted);
+    ndk::ScopedAStatus getVolumes(std::vector<float>* volumes);
     ndk::ScopedAStatus setMasterMute(bool muted);
     ndk::ScopedAStatus setMasterVolume(float volume);
     ndk::ScopedAStatus setMicGain(float gain);
@@ -60,9 +65,16 @@
     static const std::map<Control, std::vector<ControlNamesAndExpectedCtlType>> kPossibleControls;
     static Controls initializeMixerControls(struct mixer* mixer);
 
+    ndk::ScopedAStatus findControl(Control ctl, struct mixer_ctl** result);
+    ndk::ScopedAStatus getMixerControlMute(Control ctl, bool* muted);
+    ndk::ScopedAStatus getMixerControlVolume(Control ctl, float* volume);
     ndk::ScopedAStatus setMixerControlMute(Control ctl, bool muted);
     ndk::ScopedAStatus setMixerControlVolume(Control ctl, float volume);
 
+    int getMixerControlPercent(struct mixer_ctl* ctl, std::vector<int>* percents)
+            REQUIRES(mMixerAccess);
+    int getMixerControlValues(struct mixer_ctl* ctl, std::vector<int>* values)
+            REQUIRES(mMixerAccess);
     int setMixerControlPercent(struct mixer_ctl* ctl, int percent) REQUIRES(mMixerAccess);
     int setMixerControlPercent(struct mixer_ctl* ctl, const std::vector<int>& percents)
             REQUIRES(mMixerAccess);
diff --git a/audio/aidl/default/alsa/StreamAlsa.cpp b/audio/aidl/default/alsa/StreamAlsa.cpp
index 403b94b..e57d538 100644
--- a/audio/aidl/default/alsa/StreamAlsa.cpp
+++ b/audio/aidl/default/alsa/StreamAlsa.cpp
@@ -119,7 +119,7 @@
 
 ::android::status_t StreamAlsa::refinePosition(StreamDescriptor::Position* position) {
     if (mAlsaDeviceProxies.empty()) {
-        LOG(FATAL) << __func__ << ": no opened devices";
+        LOG(WARNING) << __func__ << ": no opened devices";
         return ::android::NO_INIT;
     }
     // Since the proxy can only count transferred frames since its creation,
diff --git a/audio/aidl/default/bluetooth/DevicePortProxy.cpp b/audio/aidl/default/bluetooth/DevicePortProxy.cpp
index 12e204a..1be0875 100644
--- a/audio/aidl/default/bluetooth/DevicePortProxy.cpp
+++ b/audio/aidl/default/bluetooth/DevicePortProxy.cpp
@@ -19,11 +19,25 @@
 #include <android-base/logging.h>
 #include <android-base/stringprintf.h>
 #include <audio_utils/primitives.h>
-#include <inttypes.h>
 #include <log/log.h>
 
+#include "BluetoothAudioSessionControl.h"
 #include "core-impl/DevicePortProxy.h"
 
+using aidl::android::hardware::audio::common::SinkMetadata;
+using aidl::android::hardware::audio::common::SourceMetadata;
+using aidl::android::hardware::bluetooth::audio::AudioConfiguration;
+using aidl::android::hardware::bluetooth::audio::BluetoothAudioSessionControl;
+using aidl::android::hardware::bluetooth::audio::BluetoothAudioStatus;
+using aidl::android::hardware::bluetooth::audio::ChannelMode;
+using aidl::android::hardware::bluetooth::audio::PcmConfiguration;
+using aidl::android::hardware::bluetooth::audio::PortStatusCallbacks;
+using aidl::android::hardware::bluetooth::audio::PresentationPosition;
+using aidl::android::hardware::bluetooth::audio::SessionType;
+using aidl::android::media::audio::common::AudioDeviceDescription;
+using aidl::android::media::audio::common::AudioDeviceType;
+using android::base::StringPrintf;
+
 namespace android::bluetooth::audio::aidl {
 
 namespace {
@@ -33,20 +47,6 @@
 
 }  // namespace
 
-using ::aidl::android::hardware::audio::common::SinkMetadata;
-using ::aidl::android::hardware::audio::common::SourceMetadata;
-using ::aidl::android::hardware::bluetooth::audio::AudioConfiguration;
-using ::aidl::android::hardware::bluetooth::audio::BluetoothAudioSessionControl;
-using ::aidl::android::hardware::bluetooth::audio::BluetoothAudioStatus;
-using ::aidl::android::hardware::bluetooth::audio::ChannelMode;
-using ::aidl::android::hardware::bluetooth::audio::PcmConfiguration;
-using ::aidl::android::hardware::bluetooth::audio::PortStatusCallbacks;
-using ::aidl::android::hardware::bluetooth::audio::PresentationPosition;
-using ::aidl::android::hardware::bluetooth::audio::SessionType;
-using ::aidl::android::media::audio::common::AudioDeviceDescription;
-using ::aidl::android::media::audio::common::AudioDeviceType;
-using ::android::base::StringPrintf;
-
 std::ostream& operator<<(std::ostream& os, const BluetoothStreamState& state) {
     switch (state) {
         case BluetoothStreamState::DISABLED:
diff --git a/audio/aidl/default/bluetooth/ModuleBluetooth.cpp b/audio/aidl/default/bluetooth/ModuleBluetooth.cpp
index 3c33207..502b153 100644
--- a/audio/aidl/default/bluetooth/ModuleBluetooth.cpp
+++ b/audio/aidl/default/bluetooth/ModuleBluetooth.cpp
@@ -18,50 +18,56 @@
 
 #include <android-base/logging.h>
 
-#include "BluetoothAudioSessionControl.h"
+#include "BluetoothAudioSession.h"
 #include "core-impl/ModuleBluetooth.h"
 #include "core-impl/StreamBluetooth.h"
 
-namespace aidl::android::hardware::audio::core {
+using aidl::android::hardware::audio::common::SinkMetadata;
+using aidl::android::hardware::audio::common::SourceMetadata;
+using aidl::android::media::audio::common::AudioDeviceDescription;
+using aidl::android::media::audio::common::AudioDeviceType;
+using aidl::android::media::audio::common::AudioOffloadInfo;
+using aidl::android::media::audio::common::AudioPort;
+using aidl::android::media::audio::common::AudioPortExt;
+using aidl::android::media::audio::common::MicrophoneInfo;
+using android::bluetooth::audio::aidl::BluetoothAudioPortAidl;
+using android::bluetooth::audio::aidl::BluetoothAudioPortAidlOut;
 
-using ::aidl::android::hardware::audio::common::SinkMetadata;
-using ::aidl::android::hardware::audio::common::SourceMetadata;
-using ::aidl::android::hardware::bluetooth::audio::BluetoothAudioSession;
-using ::aidl::android::media::audio::common::AudioDeviceDescription;
-using ::aidl::android::media::audio::common::AudioDeviceType;
-using ::aidl::android::media::audio::common::AudioOffloadInfo;
-using ::aidl::android::media::audio::common::AudioPort;
-using ::aidl::android::media::audio::common::AudioPortExt;
-using ::aidl::android::media::audio::common::MicrophoneInfo;
-using ::android::bluetooth::audio::aidl::BluetoothAudioPortAidl;
-using ::android::bluetooth::audio::aidl::BluetoothAudioPortAidlOut;
+namespace aidl::android::hardware::audio::core {
 
 ndk::ScopedAStatus ModuleBluetooth::getBluetoothA2dp(
         std::shared_ptr<IBluetoothA2dp>* _aidl_return) {
-    if (!mBluetoothA2dp) {
-        auto handle = ndk::SharedRefBase::make<BluetoothA2dp>();
-        handle->registerHandler(std::bind(&ModuleBluetooth::bluetoothParametersUpdated, this));
-        mBluetoothA2dp = handle;
-    }
-    *_aidl_return = mBluetoothA2dp.getInstance();
+    *_aidl_return = getBtA2dp().getInstance();
     LOG(DEBUG) << __func__ << ": returning instance of IBluetoothA2dp: " << _aidl_return->get();
     return ndk::ScopedAStatus::ok();
 }
 
 ndk::ScopedAStatus ModuleBluetooth::getBluetoothLe(std::shared_ptr<IBluetoothLe>* _aidl_return) {
+    *_aidl_return = getBtLe().getInstance();
+    LOG(DEBUG) << __func__ << ": returning instance of IBluetoothLe: " << _aidl_return->get();
+    return ndk::ScopedAStatus::ok();
+}
+
+ChildInterface<BluetoothA2dp>& ModuleBluetooth::getBtA2dp() {
+    if (!mBluetoothA2dp) {
+        auto handle = ndk::SharedRefBase::make<BluetoothA2dp>();
+        handle->registerHandler(std::bind(&ModuleBluetooth::bluetoothParametersUpdated, this));
+        mBluetoothA2dp = handle;
+    }
+    return mBluetoothA2dp;
+}
+
+ChildInterface<BluetoothLe>& ModuleBluetooth::getBtLe() {
     if (!mBluetoothLe) {
         auto handle = ndk::SharedRefBase::make<BluetoothLe>();
         handle->registerHandler(std::bind(&ModuleBluetooth::bluetoothParametersUpdated, this));
         mBluetoothLe = handle;
     }
-    *_aidl_return = mBluetoothLe.getInstance();
-    LOG(DEBUG) << __func__ << ": returning instance of IBluetoothLe: " << _aidl_return->get();
-    return ndk::ScopedAStatus::ok();
+    return mBluetoothLe;
 }
 
-Module::BtProfileHandles ModuleBluetooth::getBtProfileManagerHandles() {
-    return std::make_tuple(std::weak_ptr<IBluetooth>(), mBluetoothA2dp.getInstance(),
-                           mBluetoothLe.getInstance());
+ModuleBluetooth::BtProfileHandles ModuleBluetooth::getBtProfileManagerHandles() {
+    return std::make_tuple(std::weak_ptr<IBluetooth>(), getBtA2dp().getPtr(), getBtLe().getPtr());
 }
 
 ndk::ScopedAStatus ModuleBluetooth::getMicMute(bool* _aidl_return __unused) {
@@ -101,30 +107,35 @@
     if (description.connection == AudioDeviceDescription::CONNECTION_BT_A2DP) {
         bool isA2dpEnabled = false;
         if (!!mBluetoothA2dp) {
-            RETURN_STATUS_IF_ERROR(mBluetoothA2dp.getInstance()->isEnabled(&isA2dpEnabled));
+            RETURN_STATUS_IF_ERROR((*mBluetoothA2dp).isEnabled(&isA2dpEnabled));
         }
+        LOG(DEBUG) << __func__ << ": isA2dpEnabled: " << isA2dpEnabled;
         return isA2dpEnabled ? ndk::ScopedAStatus::ok()
                              : ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     } else if (description.connection == AudioDeviceDescription::CONNECTION_BT_LE) {
         bool isLeEnabled = false;
         if (!!mBluetoothLe) {
-            RETURN_STATUS_IF_ERROR(mBluetoothLe.getInstance()->isEnabled(&isLeEnabled));
+            RETURN_STATUS_IF_ERROR((*mBluetoothLe).isEnabled(&isLeEnabled));
         }
+        LOG(DEBUG) << __func__ << ": isLeEnabled: " << isLeEnabled;
         return isLeEnabled ? ndk::ScopedAStatus::ok()
                            : ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     } else if (description.connection == AudioDeviceDescription::CONNECTION_WIRELESS &&
                description.type == AudioDeviceType::OUT_HEARING_AID) {
         // Hearing aids can use a number of profiles, thus the only way to check
         // connectivity is to try to talk to the BT HAL.
-        if (!BluetoothAudioSession::IsAidlAvailable()) {
+        if (!::aidl::android::hardware::bluetooth::audio::BluetoothAudioSession::
+                    IsAidlAvailable()) {
             return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
         }
         std::shared_ptr<BluetoothAudioPortAidl> proxy = std::shared_ptr<BluetoothAudioPortAidl>(
                 std::make_shared<BluetoothAudioPortAidlOut>());
         if (proxy->registerPort(description)) {
+            LOG(DEBUG) << __func__ << ": registered hearing aid port";
             proxy->unregisterPort();
             return ndk::ScopedAStatus::ok();
         }
+        LOG(DEBUG) << __func__ << ": failed to register hearing aid port";
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
     }
     LOG(ERROR) << __func__ << ": unsupported device type: " << audioPort->toString();
diff --git a/audio/aidl/default/bluetooth/StreamBluetooth.cpp b/audio/aidl/default/bluetooth/StreamBluetooth.cpp
index 91a33c2..0cee7f4 100644
--- a/audio/aidl/default/bluetooth/StreamBluetooth.cpp
+++ b/audio/aidl/default/bluetooth/StreamBluetooth.cpp
@@ -20,52 +20,49 @@
 #include <android-base/logging.h>
 #include <audio_utils/clock.h>
 
-#include "BluetoothAudioSessionControl.h"
+#include "BluetoothAudioSession.h"
 #include "core-impl/StreamBluetooth.h"
 
-namespace aidl::android::hardware::audio::core {
+using aidl::android::hardware::audio::common::frameCountFromDurationUs;
+using aidl::android::hardware::audio::common::SinkMetadata;
+using aidl::android::hardware::audio::common::SourceMetadata;
+using aidl::android::hardware::audio::core::VendorParameter;
+using aidl::android::hardware::bluetooth::audio::ChannelMode;
+using aidl::android::hardware::bluetooth::audio::PcmConfiguration;
+using aidl::android::hardware::bluetooth::audio::PresentationPosition;
+using aidl::android::media::audio::common::AudioChannelLayout;
+using aidl::android::media::audio::common::AudioDevice;
+using aidl::android::media::audio::common::AudioDeviceAddress;
+using aidl::android::media::audio::common::AudioFormatDescription;
+using aidl::android::media::audio::common::AudioFormatType;
+using aidl::android::media::audio::common::AudioOffloadInfo;
+using aidl::android::media::audio::common::MicrophoneDynamicInfo;
+using aidl::android::media::audio::common::MicrophoneInfo;
+using android::bluetooth::audio::aidl::BluetoothAudioPortAidl;
+using android::bluetooth::audio::aidl::BluetoothAudioPortAidlIn;
+using android::bluetooth::audio::aidl::BluetoothAudioPortAidlOut;
+using android::bluetooth::audio::aidl::BluetoothStreamState;
 
-using ::aidl::android::hardware::audio::common::SinkMetadata;
-using ::aidl::android::hardware::audio::common::SourceMetadata;
-using ::aidl::android::hardware::audio::core::VendorParameter;
-using ::aidl::android::hardware::bluetooth::audio::ChannelMode;
-using ::aidl::android::hardware::bluetooth::audio::PcmConfiguration;
-using ::aidl::android::hardware::bluetooth::audio::PresentationPosition;
-using ::aidl::android::media::audio::common::AudioChannelLayout;
-using ::aidl::android::media::audio::common::AudioDevice;
-using ::aidl::android::media::audio::common::AudioDeviceAddress;
-using ::aidl::android::media::audio::common::AudioFormatDescription;
-using ::aidl::android::media::audio::common::AudioFormatType;
-using ::aidl::android::media::audio::common::AudioOffloadInfo;
-using ::aidl::android::media::audio::common::MicrophoneDynamicInfo;
-using ::aidl::android::media::audio::common::MicrophoneInfo;
-using ::android::bluetooth::audio::aidl::BluetoothAudioPortAidl;
-using ::android::bluetooth::audio::aidl::BluetoothAudioPortAidlIn;
-using ::android::bluetooth::audio::aidl::BluetoothAudioPortAidlOut;
-using ::android::bluetooth::audio::aidl::BluetoothStreamState;
+namespace aidl::android::hardware::audio::core {
 
 constexpr int kBluetoothDefaultInputBufferMs = 20;
 constexpr int kBluetoothDefaultOutputBufferMs = 10;
 // constexpr int kBluetoothSpatializerOutputBufferMs = 10;
 
-size_t getFrameCount(uint64_t durationUs, uint32_t sampleRate) {
-    return (durationUs * sampleRate) / 1000000;
-}
-
 // pcm configuration params are not really used by the module
 StreamBluetooth::StreamBluetooth(StreamContext* context, const Metadata& metadata,
-                                 Module::BtProfileHandles&& btHandles)
+                                 ModuleBluetooth::BtProfileHandles&& btHandles)
     : StreamCommonImpl(context, metadata),
       mSampleRate(getContext().getSampleRate()),
       mChannelLayout(getContext().getChannelLayout()),
       mFormat(getContext().getFormat()),
       mFrameSizeBytes(getContext().getFrameSize()),
       mIsInput(isInput(metadata)),
-      mBluetoothA2dp(std::move(std::get<Module::BtInterface::BTA2DP>(btHandles))),
-      mBluetoothLe(std::move(std::get<Module::BtInterface::BTLE>(btHandles))) {
+      mBluetoothA2dp(std::move(std::get<ModuleBluetooth::BtInterface::BTA2DP>(btHandles))),
+      mBluetoothLe(std::move(std::get<ModuleBluetooth::BtInterface::BTLE>(btHandles))) {
     mPreferredDataIntervalUs =
-            mIsInput ? kBluetoothDefaultInputBufferMs : kBluetoothDefaultOutputBufferMs;
-    mPreferredFrameCount = getFrameCount(mPreferredDataIntervalUs, mSampleRate);
+            (mIsInput ? kBluetoothDefaultInputBufferMs : kBluetoothDefaultOutputBufferMs) * 1000;
+    mPreferredFrameCount = frameCountFromDurationUs(mPreferredDataIntervalUs, mSampleRate);
     mIsInitialized = false;
     mIsReadyToClose = false;
 }
@@ -226,7 +223,7 @@
     if (config.dataIntervalUs > 0) {
         mPreferredDataIntervalUs =
                 std::min((int32_t)mPreferredDataIntervalUs, config.dataIntervalUs);
-        mPreferredFrameCount = getFrameCount(mPreferredDataIntervalUs, mSampleRate);
+        mPreferredFrameCount = frameCountFromDurationUs(mPreferredDataIntervalUs, mSampleRate);
     }
     return true;
 }
@@ -318,7 +315,7 @@
 
 StreamInBluetooth::StreamInBluetooth(StreamContext&& context, const SinkMetadata& sinkMetadata,
                                      const std::vector<MicrophoneInfo>& microphones,
-                                     Module::BtProfileHandles&& btProfileHandles)
+                                     ModuleBluetooth::BtProfileHandles&& btProfileHandles)
     : StreamIn(std::move(context), microphones),
       StreamBluetooth(&mContextInstance, sinkMetadata, std::move(btProfileHandles)) {}
 
@@ -331,7 +328,7 @@
 StreamOutBluetooth::StreamOutBluetooth(StreamContext&& context,
                                        const SourceMetadata& sourceMetadata,
                                        const std::optional<AudioOffloadInfo>& offloadInfo,
-                                       Module::BtProfileHandles&& btProfileHandles)
+                                       ModuleBluetooth::BtProfileHandles&& btProfileHandles)
     : StreamOut(std::move(context), offloadInfo),
       StreamBluetooth(&mContextInstance, sourceMetadata, std::move(btProfileHandles)) {}
 
diff --git a/audio/aidl/default/include/core-impl/Bluetooth.h b/audio/aidl/default/include/core-impl/Bluetooth.h
index 44899bc..002cb19 100644
--- a/audio/aidl/default/include/core-impl/Bluetooth.h
+++ b/audio/aidl/default/include/core-impl/Bluetooth.h
@@ -46,9 +46,9 @@
 class BluetoothA2dp : public BnBluetoothA2dp, public ParamChangeHandler {
   public:
     BluetoothA2dp() = default;
+    ndk::ScopedAStatus isEnabled(bool* _aidl_return) override;
 
   private:
-    ndk::ScopedAStatus isEnabled(bool* _aidl_return) override;
     ndk::ScopedAStatus setEnabled(bool in_enabled) override;
     ndk::ScopedAStatus supportsOffloadReconfiguration(bool* _aidl_return) override;
     ndk::ScopedAStatus reconfigureOffload(
@@ -61,9 +61,9 @@
 class BluetoothLe : public BnBluetoothLe, public ParamChangeHandler {
   public:
     BluetoothLe() = default;
+    ndk::ScopedAStatus isEnabled(bool* _aidl_return) override;
 
   private:
-    ndk::ScopedAStatus isEnabled(bool* _aidl_return) override;
     ndk::ScopedAStatus setEnabled(bool in_enabled) override;
     ndk::ScopedAStatus supportsOffloadReconfiguration(bool* _aidl_return) override;
     ndk::ScopedAStatus reconfigureOffload(
diff --git a/audio/aidl/default/include/core-impl/ChildInterface.h b/audio/aidl/default/include/core-impl/ChildInterface.h
index 3b74c5e..f5f1855 100644
--- a/audio/aidl/default/include/core-impl/ChildInterface.h
+++ b/audio/aidl/default/include/core-impl/ChildInterface.h
@@ -40,6 +40,7 @@
     explicit operator bool() const { return !!this->first; }
     C& operator*() const { return *(this->first); }
     C* operator->() const { return this->first; }
+    std::shared_ptr<C> getPtr() { return this->first; }
     // Use 'getInstance' when returning the interface instance.
     std::shared_ptr<C> getInstance() {
         (void)getBinder();
diff --git a/audio/aidl/default/include/core-impl/DevicePortProxy.h b/audio/aidl/default/include/core-impl/DevicePortProxy.h
index 13b8c91..17a8cf3 100644
--- a/audio/aidl/default/include/core-impl/DevicePortProxy.h
+++ b/audio/aidl/default/include/core-impl/DevicePortProxy.h
@@ -25,11 +25,10 @@
 #include <aidl/android/hardware/audio/common/SourceMetadata.h>
 #include <aidl/android/hardware/bluetooth/audio/BluetoothAudioStatus.h>
 #include <aidl/android/hardware/bluetooth/audio/PcmConfiguration.h>
+#include <aidl/android/hardware/bluetooth/audio/PresentationPosition.h>
 #include <aidl/android/hardware/bluetooth/audio/SessionType.h>
 #include <aidl/android/media/audio/common/AudioDeviceDescription.h>
 
-#include "BluetoothAudioSessionControl.h"
-
 namespace android::bluetooth::audio::aidl {
 
 enum class BluetoothStreamState : uint8_t {
@@ -239,4 +238,4 @@
     size_t readData(void* buffer, size_t bytes) const override;
 };
 
-}  // namespace android::bluetooth::audio::aidl
\ No newline at end of file
+}  // namespace android::bluetooth::audio::aidl
diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h
index d92d54b..caf43f1 100644
--- a/audio/aidl/default/include/core-impl/Module.h
+++ b/audio/aidl/default/include/core-impl/Module.h
@@ -22,6 +22,7 @@
 #include <optional>
 #include <set>
 
+#include <Utils.h>
 #include <aidl/android/hardware/audio/core/BnModule.h>
 
 #include "core-impl/ChildInterface.h"
@@ -45,13 +46,6 @@
         int32_t nextPatchId = 1;
     };
     enum Type : int { DEFAULT, R_SUBMIX, STUB, USB, BLUETOOTH };
-    enum BtInterface : int { BTCONF, BTA2DP, BTLE };
-    typedef std::tuple<std::weak_ptr<IBluetooth>, std::weak_ptr<IBluetoothA2dp>,
-                       std::weak_ptr<IBluetoothLe>>
-            BtProfileHandles;
-
-    // This value is used by default for all AudioPatches and reported by all streams.
-    static constexpr int32_t kLatencyMs = 10;
 
     static std::shared_ptr<Module> createInstance(Type type) {
         return createInstance(type, std::make_unique<Configuration>());
@@ -145,8 +139,6 @@
     ndk::ScopedAStatus getAAudioMixerBurstCount(int32_t* _aidl_return) override;
     ndk::ScopedAStatus getAAudioHardwareBurstMinUsec(int32_t* _aidl_return) override;
 
-    // This value is used for all AudioPatches.
-    static constexpr int32_t kMinimumStreamBufferSizeFrames = 256;
     // The maximum stream buffer size is 1 GiB = 2 ** 30 bytes;
     static constexpr int32_t kMaximumStreamBufferSizeBytes = 1 << 30;
 
@@ -207,8 +199,19 @@
     virtual ndk::ScopedAStatus onMasterVolumeChanged(float volume);
     virtual std::vector<::aidl::android::media::audio::common::MicrophoneInfo> getMicrophoneInfos();
     virtual std::unique_ptr<Configuration> initializeConfig();
+    virtual int32_t getNominalLatencyMs(
+            const ::aidl::android::media::audio::common::AudioPortConfig& portConfig);
 
     // Utility and helper functions accessible to subclasses.
+    static int32_t calculateBufferSizeFrames(int32_t latencyMs, int32_t sampleRateHz) {
+        const int32_t rawSizeFrames =
+                aidl::android::hardware::audio::common::frameCountFromDurationMs(latencyMs,
+                                                                                 sampleRateHz);
+        int32_t powerOf2 = 1;
+        while (powerOf2 < rawSizeFrames) powerOf2 <<= 1;
+        return powerOf2;
+    }
+
     ndk::ScopedAStatus bluetoothParametersUpdated();
     void cleanUpPatch(int32_t patchId);
     ndk::ScopedAStatus createStreamContext(
@@ -222,7 +225,6 @@
     ndk::ScopedAStatus findPortIdForNewStream(
             int32_t in_portConfigId, ::aidl::android::media::audio::common::AudioPort** port);
     std::vector<AudioRoute*> getAudioRoutesForAudioPortImpl(int32_t portId);
-    virtual BtProfileHandles getBtProfileManagerHandles();
     Configuration& getConfig();
     const ConnectedDevicePorts& getConnectedDevicePorts() const { return mConnectedDevicePorts; }
     bool getMasterMute() const { return mMasterMute; }
diff --git a/audio/aidl/default/include/core-impl/ModuleBluetooth.h b/audio/aidl/default/include/core-impl/ModuleBluetooth.h
index 7ac2d34..a58798b 100644
--- a/audio/aidl/default/include/core-impl/ModuleBluetooth.h
+++ b/audio/aidl/default/include/core-impl/ModuleBluetooth.h
@@ -23,11 +23,18 @@
 
 class ModuleBluetooth final : public Module {
   public:
+    enum BtInterface : int { BTSCO, BTA2DP, BTLE };
+    typedef std::tuple<std::weak_ptr<IBluetooth>, std::weak_ptr<IBluetoothA2dp>,
+                       std::weak_ptr<IBluetoothLe>>
+            BtProfileHandles;
+
     ModuleBluetooth(std::unique_ptr<Configuration>&& config)
         : Module(Type::BLUETOOTH, std::move(config)) {}
 
   private:
-    BtProfileHandles getBtProfileManagerHandles() override;
+    ChildInterface<BluetoothA2dp>& getBtA2dp();
+    ChildInterface<BluetoothLe>& getBtLe();
+    BtProfileHandles getBtProfileManagerHandles();
 
     ndk::ScopedAStatus getBluetoothA2dp(std::shared_ptr<IBluetoothA2dp>* _aidl_return) override;
     ndk::ScopedAStatus getBluetoothLe(std::shared_ptr<IBluetoothLe>* _aidl_return) override;
@@ -50,8 +57,8 @@
     ndk::ScopedAStatus onMasterMuteChanged(bool mute) override;
     ndk::ScopedAStatus onMasterVolumeChanged(float volume) override;
 
-    ChildInterface<IBluetoothA2dp> mBluetoothA2dp;
-    ChildInterface<IBluetoothLe> mBluetoothLe;
+    ChildInterface<BluetoothA2dp> mBluetoothA2dp;
+    ChildInterface<BluetoothLe> mBluetoothLe;
 };
 
 }  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/include/core-impl/ModulePrimary.h b/audio/aidl/default/include/core-impl/ModulePrimary.h
index ee86d64..82c8a03 100644
--- a/audio/aidl/default/include/core-impl/ModulePrimary.h
+++ b/audio/aidl/default/include/core-impl/ModulePrimary.h
@@ -39,6 +39,8 @@
             const std::optional<::aidl::android::media::audio::common::AudioOffloadInfo>&
                     offloadInfo,
             std::shared_ptr<StreamOut>* result) override;
+    int32_t getNominalLatencyMs(
+            const ::aidl::android::media::audio::common::AudioPortConfig& portConfig) override;
 
   private:
     ChildInterface<ITelephony> mTelephony;
diff --git a/audio/aidl/default/include/core-impl/ModuleRemoteSubmix.h b/audio/aidl/default/include/core-impl/ModuleRemoteSubmix.h
index ebf4558..9f8acc9 100644
--- a/audio/aidl/default/include/core-impl/ModuleRemoteSubmix.h
+++ b/audio/aidl/default/include/core-impl/ModuleRemoteSubmix.h
@@ -50,6 +50,9 @@
             override;
     ndk::ScopedAStatus onMasterMuteChanged(bool mute) override;
     ndk::ScopedAStatus onMasterVolumeChanged(float volume) override;
+    int32_t getNominalLatencyMs(
+            const ::aidl::android::media::audio::common::AudioPortConfig& portConfig) override;
+    // TODO(b/307586684): Report proper minimum stream buffer size by overriding 'setAudioPatch'.
 };
 
 }  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/include/core-impl/Stream.h b/audio/aidl/default/include/core-impl/Stream.h
index daa920d..aa9fb19 100644
--- a/audio/aidl/default/include/core-impl/Stream.h
+++ b/audio/aidl/default/include/core-impl/Stream.h
@@ -81,11 +81,10 @@
 
     StreamContext() = default;
     StreamContext(std::unique_ptr<CommandMQ> commandMQ, std::unique_ptr<ReplyMQ> replyMQ,
-                  int portId,
                   const ::aidl::android::media::audio::common::AudioFormatDescription& format,
                   const ::aidl::android::media::audio::common::AudioChannelLayout& channelLayout,
                   int sampleRate, const ::aidl::android::media::audio::common::AudioIoFlags& flags,
-                  int32_t mixPortHandle, std::unique_ptr<DataMQ> dataMQ,
+                  int32_t nominalLatencyMs, int32_t mixPortHandle, std::unique_ptr<DataMQ> dataMQ,
                   std::shared_ptr<IStreamCallback> asyncCallback,
                   std::shared_ptr<IStreamOutEventCallback> outEventCallback,
                   std::weak_ptr<sounddose::StreamDataProcessorInterface> streamDataProcessor,
@@ -93,51 +92,17 @@
         : mCommandMQ(std::move(commandMQ)),
           mInternalCommandCookie(std::rand()),
           mReplyMQ(std::move(replyMQ)),
-          mPortId(portId),
           mFormat(format),
           mChannelLayout(channelLayout),
           mSampleRate(sampleRate),
           mFlags(flags),
+          mNominalLatencyMs(nominalLatencyMs),
           mMixPortHandle(mixPortHandle),
           mDataMQ(std::move(dataMQ)),
           mAsyncCallback(asyncCallback),
           mOutEventCallback(outEventCallback),
           mStreamDataProcessor(streamDataProcessor),
           mDebugParameters(debugParameters) {}
-    StreamContext(StreamContext&& other)
-        : mCommandMQ(std::move(other.mCommandMQ)),
-          mInternalCommandCookie(other.mInternalCommandCookie),
-          mReplyMQ(std::move(other.mReplyMQ)),
-          mPortId(other.mPortId),
-          mFormat(other.mFormat),
-          mChannelLayout(other.mChannelLayout),
-          mSampleRate(other.mSampleRate),
-          mFlags(std::move(other.mFlags)),
-          mMixPortHandle(other.mMixPortHandle),
-          mDataMQ(std::move(other.mDataMQ)),
-          mAsyncCallback(std::move(other.mAsyncCallback)),
-          mOutEventCallback(std::move(other.mOutEventCallback)),
-          mStreamDataProcessor(std::move(other.mStreamDataProcessor)),
-          mDebugParameters(std::move(other.mDebugParameters)),
-          mFrameCount(other.mFrameCount) {}
-    StreamContext& operator=(StreamContext&& other) {
-        mCommandMQ = std::move(other.mCommandMQ);
-        mInternalCommandCookie = other.mInternalCommandCookie;
-        mReplyMQ = std::move(other.mReplyMQ);
-        mPortId = std::move(other.mPortId);
-        mFormat = std::move(other.mFormat);
-        mChannelLayout = std::move(other.mChannelLayout);
-        mSampleRate = other.mSampleRate;
-        mFlags = std::move(other.mFlags);
-        mMixPortHandle = other.mMixPortHandle;
-        mDataMQ = std::move(other.mDataMQ);
-        mAsyncCallback = std::move(other.mAsyncCallback);
-        mOutEventCallback = std::move(other.mOutEventCallback);
-        mStreamDataProcessor = std::move(other.mStreamDataProcessor);
-        mDebugParameters = std::move(other.mDebugParameters);
-        mFrameCount = other.mFrameCount;
-        return *this;
-    }
 
     void fillDescriptor(StreamDescriptor* desc);
     std::shared_ptr<IStreamCallback> getAsyncCallback() const { return mAsyncCallback; }
@@ -156,6 +121,7 @@
     size_t getFrameSize() const;
     int getInternalCommandCookie() const { return mInternalCommandCookie; }
     int32_t getMixPortHandle() const { return mMixPortHandle; }
+    int32_t getNominalLatencyMs() const { return mNominalLatencyMs; }
     std::shared_ptr<IStreamOutEventCallback> getOutEventCallback() const {
         return mOutEventCallback;
     }
@@ -163,7 +129,6 @@
         return mStreamDataProcessor;
     }
     void startStreamDataProcessor();
-    int getPortId() const { return mPortId; }
     ReplyMQ* getReplyMQ() const { return mReplyMQ.get(); }
     int getTransientStateDelayMs() const { return mDebugParameters.transientStateDelayMs; }
     int getSampleRate() const { return mSampleRate; }
@@ -176,14 +141,15 @@
     long getFrameCount() const { return mFrameCount; }
 
   private:
+    // Fields are non const to allow move assignment.
     std::unique_ptr<CommandMQ> mCommandMQ;
     int mInternalCommandCookie;  // The value used to confirm that the command was posted internally
     std::unique_ptr<ReplyMQ> mReplyMQ;
-    int mPortId;
     ::aidl::android::media::audio::common::AudioFormatDescription mFormat;
     ::aidl::android::media::audio::common::AudioChannelLayout mChannelLayout;
     int mSampleRate;
     ::aidl::android::media::audio::common::AudioIoFlags mFlags;
+    int32_t mNominalLatencyMs;
     int32_t mMixPortHandle;
     std::unique_ptr<DataMQ> mDataMQ;
     std::shared_ptr<IStreamCallback> mAsyncCallback;
diff --git a/audio/aidl/default/include/core-impl/StreamBluetooth.h b/audio/aidl/default/include/core-impl/StreamBluetooth.h
index c2f8c1d..1258d38 100644
--- a/audio/aidl/default/include/core-impl/StreamBluetooth.h
+++ b/audio/aidl/default/include/core-impl/StreamBluetooth.h
@@ -24,7 +24,7 @@
 #include <aidl/android/hardware/audio/core/IBluetoothLe.h>
 
 #include "core-impl/DevicePortProxy.h"
-#include "core-impl/Module.h"
+#include "core-impl/ModuleBluetooth.h"
 #include "core-impl/Stream.h"
 
 namespace aidl::android::hardware::audio::core {
@@ -32,7 +32,7 @@
 class StreamBluetooth : public StreamCommonImpl {
   public:
     StreamBluetooth(StreamContext* context, const Metadata& metadata,
-                    Module::BtProfileHandles&& btHandles);
+                    ModuleBluetooth::BtProfileHandles&& btHandles);
     // Methods of 'DriverInterface'.
     ::android::status_t init() override;
     ::android::status_t drain(StreamDescriptor::DrainMode) override;
@@ -80,7 +80,7 @@
             StreamContext&& context,
             const ::aidl::android::hardware::audio::common::SinkMetadata& sinkMetadata,
             const std::vector<::aidl::android::media::audio::common::MicrophoneInfo>& microphones,
-            Module::BtProfileHandles&& btHandles);
+            ModuleBluetooth::BtProfileHandles&& btHandles);
 
   private:
     void onClose(StreamDescriptor::State) override { defaultOnClose(); }
@@ -97,7 +97,7 @@
             const ::aidl::android::hardware::audio::common::SourceMetadata& sourceMetadata,
             const std::optional<::aidl::android::media::audio::common::AudioOffloadInfo>&
                     offloadInfo,
-            Module::BtProfileHandles&& btHandles);
+            ModuleBluetooth::BtProfileHandles&& btHandles);
 
   private:
     void onClose(StreamDescriptor::State) override { defaultOnClose(); }
diff --git a/audio/aidl/default/include/core-impl/StreamPrimary.h b/audio/aidl/default/include/core-impl/StreamPrimary.h
index b64b749..abc119c 100644
--- a/audio/aidl/default/include/core-impl/StreamPrimary.h
+++ b/audio/aidl/default/include/core-impl/StreamPrimary.h
@@ -27,10 +27,13 @@
   public:
     StreamPrimary(StreamContext* context, const Metadata& metadata);
 
+    ::android::status_t transfer(void* buffer, size_t frameCount, size_t* actualFrameCount,
+                                 int32_t* latencyMs) override;
+
   protected:
     std::vector<alsa::DeviceProfile> getDeviceProfiles() override;
 
-    const bool mIsInput;
+    const bool mIsAsynchronous;
 };
 
 class StreamInPrimary final : public StreamIn, public StreamSwitcher, public StreamInHwGainHelper {
diff --git a/audio/aidl/default/include/core-impl/StreamRemoteSubmix.h b/audio/aidl/default/include/core-impl/StreamRemoteSubmix.h
index 94404a1..21592b3 100644
--- a/audio/aidl/default/include/core-impl/StreamRemoteSubmix.h
+++ b/audio/aidl/default/include/core-impl/StreamRemoteSubmix.h
@@ -46,7 +46,7 @@
     ndk::ScopedAStatus prepareToClose() override;
 
   private:
-    size_t getPipeSizeInFrames();
+    long getDelayInUsForFrameCount(size_t frameCount);
     size_t getStreamPipeSizeInFrames();
     ::android::status_t outWrite(void* buffer, size_t frameCount, size_t* actualFrameCount);
     ::android::status_t inRead(void* buffer, size_t frameCount, size_t* actualFrameCount);
diff --git a/audio/aidl/default/primary/StreamPrimary.cpp b/audio/aidl/default/primary/StreamPrimary.cpp
index 17de2ba..7e3bdd4 100644
--- a/audio/aidl/default/primary/StreamPrimary.cpp
+++ b/audio/aidl/default/primary/StreamPrimary.cpp
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-#include <limits>
+#include <chrono>
 
 #define LOG_TAG "AHAL_StreamPrimary"
 #include <android-base/logging.h>
 #include <android-base/properties.h>
+#include <audio_utils/clock.h>
 #include <error/expected_utils.h>
 
 #include "PrimaryMixer.h"
@@ -37,10 +38,34 @@
 namespace aidl::android::hardware::audio::core {
 
 StreamPrimary::StreamPrimary(StreamContext* context, const Metadata& metadata)
-    : StreamAlsa(context, metadata, 3 /*readWriteRetries*/), mIsInput(isInput(metadata)) {
+    : StreamAlsa(context, metadata, 3 /*readWriteRetries*/),
+      mIsAsynchronous(!!getContext().getAsyncCallback()) {
     context->startStreamDataProcessor();
 }
 
+::android::status_t StreamPrimary::transfer(void* buffer, size_t frameCount,
+                                            size_t* actualFrameCount, int32_t* latencyMs) {
+    auto start = std::chrono::steady_clock::now();
+    if (auto status = StreamAlsa::transfer(buffer, frameCount, actualFrameCount, latencyMs);
+        status != ::android::OK) {
+        return status;
+    }
+    // This is a workaround for the emulator implementation which has a host-side buffer
+    // and this can result in reading faster than real time.
+    if (mIsInput && !mIsAsynchronous) {
+        auto recordDurationUs = std::chrono::duration_cast<std::chrono::microseconds>(
+                std::chrono::steady_clock::now() - start);
+        const long projectedVsObservedOffsetUs =
+                *actualFrameCount * MICROS_PER_SECOND / mContext.getSampleRate() -
+                recordDurationUs.count();
+        if (projectedVsObservedOffsetUs > 0) {
+            LOG(VERBOSE) << __func__ << ": sleeping for " << projectedVsObservedOffsetUs << " us";
+            usleep(projectedVsObservedOffsetUs);
+        }
+    }
+    return ::android::OK;
+}
+
 std::vector<alsa::DeviceProfile> StreamPrimary::getDeviceProfiles() {
     static const std::vector<alsa::DeviceProfile> kBuiltInSource{
             alsa::DeviceProfile{.card = primary::PrimaryMixer::kAlsaCard,
@@ -66,7 +91,8 @@
             GetBoolProperty("ro.boot.audio.tinyalsa.simulate_input", false);
     return kSimulateInput || device.type.type == AudioDeviceType::IN_TELEPHONY_RX ||
            device.type.type == AudioDeviceType::IN_FM_TUNER ||
-           device.type.connection == AudioDeviceDescription::CONNECTION_BUS;
+           device.type.connection == AudioDeviceDescription::CONNECTION_BUS ||
+           (device.type.type == AudioDeviceType::IN_DEVICE && device.type.connection.empty());
 }
 
 StreamSwitcher::DeviceSwitchBehavior StreamInPrimary::switchCurrentStream(
@@ -101,6 +127,11 @@
     if (isStubStream()) {
         return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
     }
+    float gain;
+    RETURN_STATUS_IF_ERROR(primary::PrimaryMixer::getInstance().getMicGain(&gain));
+    _aidl_return->resize(0);
+    _aidl_return->resize(mChannelCount, gain);
+    RETURN_STATUS_IF_ERROR(setHwGainImpl(*_aidl_return));
     return getHwGainImpl(_aidl_return);
 }
 
@@ -132,7 +163,8 @@
     static const bool kSimulateOutput =
             GetBoolProperty("ro.boot.audio.tinyalsa.ignore_output", false);
     return kSimulateOutput || device.type.type == AudioDeviceType::OUT_TELEPHONY_TX ||
-           device.type.connection == AudioDeviceDescription::CONNECTION_BUS;
+           device.type.connection == AudioDeviceDescription::CONNECTION_BUS ||
+           (device.type.type == AudioDeviceType::OUT_DEVICE && device.type.connection.empty());
 }
 
 StreamSwitcher::DeviceSwitchBehavior StreamOutPrimary::switchCurrentStream(
@@ -167,6 +199,9 @@
     if (isStubStream()) {
         return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
     }
+    RETURN_STATUS_IF_ERROR(primary::PrimaryMixer::getInstance().getVolumes(_aidl_return));
+    _aidl_return->resize(mChannelCount);
+    RETURN_STATUS_IF_ERROR(setHwVolumeImpl(*_aidl_return));
     return getHwVolumeImpl(_aidl_return);
 }
 
diff --git a/audio/aidl/default/r_submix/ModuleRemoteSubmix.cpp b/audio/aidl/default/r_submix/ModuleRemoteSubmix.cpp
index f8c775f..3e8dd7c 100644
--- a/audio/aidl/default/r_submix/ModuleRemoteSubmix.cpp
+++ b/audio/aidl/default/r_submix/ModuleRemoteSubmix.cpp
@@ -21,6 +21,7 @@
 #include <android-base/logging.h>
 #include <error/expected_utils.h>
 
+#include "SubmixRoute.h"
 #include "core-impl/ModuleRemoteSubmix.h"
 #include "core-impl/StreamRemoteSubmix.h"
 
@@ -106,4 +107,12 @@
     return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
 }
 
+int32_t ModuleRemoteSubmix::getNominalLatencyMs(const AudioPortConfig&) {
+    // See the note on kDefaultPipePeriodCount.
+    static constexpr int32_t kMaxLatencyMs =
+            (r_submix::kDefaultPipeSizeInFrames * 1000) / r_submix::kDefaultSampleRateHz;
+    static constexpr int32_t kMinLatencyMs = kMaxLatencyMs / r_submix::kDefaultPipePeriodCount;
+    return (kMaxLatencyMs + kMinLatencyMs) / 2;
+}
+
 }  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp b/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp
index 9c9c08b..38281b9 100644
--- a/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp
+++ b/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp
@@ -17,8 +17,6 @@
 #define LOG_TAG "AHAL_StreamRemoteSubmix"
 #include <android-base/logging.h>
 
-#include <cmath>
-
 #include "core-impl/StreamRemoteSubmix.h"
 
 using aidl::android::hardware::audio::common::SinkMetadata;
@@ -158,27 +156,8 @@
 
 ::android::status_t StreamRemoteSubmix::transfer(void* buffer, size_t frameCount,
                                                  size_t* actualFrameCount, int32_t* latencyMs) {
-    *latencyMs = (getStreamPipeSizeInFrames() * MILLIS_PER_SECOND) / mStreamConfig.sampleRate;
+    *latencyMs = getDelayInUsForFrameCount(getStreamPipeSizeInFrames()) / 1000;
     LOG(VERBOSE) << __func__ << ": Latency " << *latencyMs << "ms";
-
-    sp<MonoPipe> sink = mCurrentRoute->getSink();
-    if (sink != nullptr) {
-        if (sink->isShutdown()) {
-            sink.clear();
-            LOG(VERBOSE) << __func__ << ": pipe shutdown, ignoring the transfer.";
-            // the pipe has already been shutdown, this buffer will be lost but we must simulate
-            // timing so we don't drain the output faster than realtime
-            const size_t delayUs = static_cast<size_t>(
-                    std::roundf(frameCount * MICROS_PER_SECOND / mStreamConfig.sampleRate));
-            usleep(delayUs);
-
-            *actualFrameCount = frameCount;
-            return ::android::OK;
-        }
-    } else {
-        LOG(ERROR) << __func__ << ": transfer without a pipe!";
-        return ::android::UNEXPECTED_NULL;
-    }
     mCurrentRoute->exitStandby(mIsInput);
     return (mIsInput ? inRead(buffer, frameCount, actualFrameCount)
                      : outWrite(buffer, frameCount, actualFrameCount));
@@ -202,6 +181,10 @@
     return ::android::OK;
 }
 
+long StreamRemoteSubmix::getDelayInUsForFrameCount(size_t frameCount) {
+    return frameCount * MICROS_PER_SECOND / mStreamConfig.sampleRate;
+}
+
 // Calculate the maximum size of the pipe buffer in frames for the specified stream.
 size_t StreamRemoteSubmix::getStreamPipeSizeInFrames() {
     auto pipeConfig = mCurrentRoute->mPipeConfig;
@@ -215,11 +198,11 @@
     if (sink != nullptr) {
         if (sink->isShutdown()) {
             sink.clear();
-            LOG(VERBOSE) << __func__ << ": pipe shutdown, ignoring the write.";
+            const auto delayUs = getDelayInUsForFrameCount(frameCount);
+            LOG(DEBUG) << __func__ << ": pipe shutdown, ignoring the write, sleeping for "
+                       << delayUs << " us";
             // the pipe has already been shutdown, this buffer will be lost but we must
             // simulate timing so we don't drain the output faster than realtime
-            const size_t delayUs = static_cast<size_t>(
-                    std::roundf(frameCount * MICROS_PER_SECOND / mStreamConfig.sampleRate));
             usleep(delayUs);
             *actualFrameCount = frameCount;
             return ::android::OK;
@@ -229,17 +212,18 @@
         return ::android::UNKNOWN_ERROR;
     }
 
-    const size_t availableToWrite = sink->availableToWrite();
+    const bool shouldBlockWrite = mCurrentRoute->shouldBlockWrite();
+    size_t availableToWrite = sink->availableToWrite();
     // NOTE: sink has been checked above and sink and source life cycles are synchronized
     sp<MonoPipeReader> source = mCurrentRoute->getSource();
     // If the write to the sink should be blocked, flush enough frames from the pipe to make space
     // to write the most recent data.
-    if (!mCurrentRoute->shouldBlockWrite() && availableToWrite < frameCount) {
+    if (!shouldBlockWrite && availableToWrite < frameCount) {
         static uint8_t flushBuffer[64];
         const size_t flushBufferSizeFrames = sizeof(flushBuffer) / mStreamConfig.frameSize;
         size_t framesToFlushFromSource = frameCount - availableToWrite;
-        LOG(VERBOSE) << __func__ << ": flushing " << framesToFlushFromSource
-                     << " frames from the pipe to avoid blocking";
+        LOG(DEBUG) << __func__ << ": flushing " << framesToFlushFromSource
+                   << " frames from the pipe to avoid blocking";
         while (framesToFlushFromSource) {
             const size_t flushSize = std::min(framesToFlushFromSource, flushBufferSizeFrames);
             framesToFlushFromSource -= flushSize;
@@ -247,7 +231,12 @@
             source->read(flushBuffer, flushSize);
         }
     }
+    availableToWrite = sink->availableToWrite();
 
+    if (!shouldBlockWrite && frameCount > availableToWrite) {
+        // Truncate the request to avoid blocking.
+        frameCount = availableToWrite;
+    }
     ssize_t writtenFrames = sink->write(buffer, frameCount);
     if (writtenFrames < 0) {
         if (writtenFrames == (ssize_t)::android::NEGOTIATE) {
@@ -261,7 +250,6 @@
             writtenFrames = sink->write(buffer, frameCount);
         }
     }
-    sink.clear();
 
     if (writtenFrames < 0) {
         LOG(ERROR) << __func__ << ": failed writing to pipe with " << writtenFrames;
@@ -286,8 +274,9 @@
         } else {
             LOG(ERROR) << __func__ << ": Read errors " << readErrorCount;
         }
-        const size_t delayUs = static_cast<size_t>(
-                std::roundf(frameCount * MICROS_PER_SECOND / mStreamConfig.sampleRate));
+        const auto delayUs = getDelayInUsForFrameCount(frameCount);
+        LOG(DEBUG) << __func__ << ": no source, ignoring the read, sleeping for " << delayUs
+                   << " us";
         usleep(delayUs);
         memset(buffer, 0, mStreamConfig.frameSize * frameCount);
         *actualFrameCount = frameCount;
@@ -296,7 +285,7 @@
 
     // read the data from the pipe
     int attempts = 0;
-    const size_t delayUs = static_cast<size_t>(std::roundf(kReadAttemptSleepUs));
+    const long delayUs = kReadAttemptSleepUs;
     char* buff = (char*)buffer;
     size_t remainingFrames = frameCount;
     int availableToRead = source->availableToRead();
@@ -313,11 +302,12 @@
             buff += framesRead * mStreamConfig.frameSize;
             availableToRead -= framesRead;
             LOG(VERBOSE) << __func__ << ": (attempts = " << attempts << ") got " << framesRead
-                         << " frames, remaining=" << remainingFrames;
+                         << " frames, remaining =" << remainingFrames;
         } else {
             attempts++;
             LOG(WARNING) << __func__ << ": read returned " << framesRead
-                         << " , read failure attempts = " << attempts;
+                         << " , read failure attempts = " << attempts << ", sleeping for "
+                         << delayUs << " us";
             usleep(delayUs);
         }
     }
@@ -337,18 +327,18 @@
     // compute how much we need to sleep after reading the data by comparing the wall clock with
     //   the projected time at which we should return.
     // wall clock after reading from the pipe
-    auto recordDurationUs = std::chrono::steady_clock::now() - mCurrentRoute->getRecordStartTime();
+    auto recordDurationUs = std::chrono::duration_cast<std::chrono::microseconds>(
+            std::chrono::steady_clock::now() - mCurrentRoute->getRecordStartTime());
 
     // readCounterFrames contains the number of frames that have been read since the beginning of
     // recording (including this call): it's converted to usec and compared to how long we've been
     // recording for, which gives us how long we must wait to sync the projected recording time, and
     // the observed recording time.
-    const int projectedVsObservedOffsetUs =
-            std::roundf((readCounterFrames * MICROS_PER_SECOND / mStreamConfig.sampleRate) -
-                        recordDurationUs.count());
+    const long projectedVsObservedOffsetUs =
+            getDelayInUsForFrameCount(readCounterFrames) - recordDurationUs.count();
 
     LOG(VERBOSE) << __func__ << ": record duration " << recordDurationUs.count()
-                 << " microseconds, will wait: " << projectedVsObservedOffsetUs << " microseconds";
+                 << " us, will wait: " << projectedVsObservedOffsetUs << " us";
     if (projectedVsObservedOffsetUs > 0) {
         usleep(projectedVsObservedOffsetUs);
     }
diff --git a/audio/aidl/default/r_submix/SubmixRoute.h b/audio/aidl/default/r_submix/SubmixRoute.h
index 1a98df2..92b95e9 100644
--- a/audio/aidl/default/r_submix/SubmixRoute.h
+++ b/audio/aidl/default/r_submix/SubmixRoute.h
@@ -14,16 +14,19 @@
  * limitations under the License.
  */
 
+#pragma once
+
+#include <chrono>
 #include <mutex>
 
+#include <android-base/thread_annotations.h>
 #include <audio_utils/clock.h>
 
 #include <media/nbaio/MonoPipe.h>
 #include <media/nbaio/MonoPipeReader.h>
 
 #include <aidl/android/media/audio/common/AudioChannelLayout.h>
-
-#include "core-impl/Stream.h"
+#include <aidl/android/media/audio/common/AudioFormatDescription.h>
 
 using aidl::android::media::audio::common::AudioChannelLayout;
 using aidl::android::media::audio::common::AudioFormatDescription;
@@ -36,9 +39,13 @@
 namespace aidl::android::hardware::audio::core::r_submix {
 
 static constexpr int kDefaultSampleRateHz = 48000;
-// Size at default sample rate
-// NOTE: This value will be rounded up to the nearest power of 2 by MonoPipe().
-static constexpr int kDefaultPipeSizeInFrames = (1024 * 4);
+// Value used to divide the MonoPipe buffer into segments that are written to the source and
+// read from the sink. The maximum latency of the device is the size of the MonoPipe's buffer
+// the minimum latency is the MonoPipe buffer size divided by this value.
+static constexpr int kDefaultPipePeriodCount = 4;
+// Size at the default sample rate
+// NOTE: This value will be rounded up to the nearest power of 2 by MonoPipe.
+static constexpr int kDefaultPipeSizeInFrames = 1024 * kDefaultPipePeriodCount;
 
 // Configuration of the audio stream.
 struct AudioConfig {
diff --git a/audio/aidl/default/stub/StreamStub.cpp b/audio/aidl/default/stub/StreamStub.cpp
index 660a51e..2422fe4 100644
--- a/audio/aidl/default/stub/StreamStub.cpp
+++ b/audio/aidl/default/stub/StreamStub.cpp
@@ -94,7 +94,7 @@
 }
 
 ::android::status_t StreamStub::transfer(void* buffer, size_t frameCount, size_t* actualFrameCount,
-                                         int32_t* latencyMs) {
+                                         int32_t*) {
     if (!mIsInitialized) {
         LOG(FATAL) << __func__ << ": must not happen for an uninitialized driver";
     }
@@ -117,7 +117,6 @@
         }
     }
     *actualFrameCount = frameCount;
-    *latencyMs = Module::kLatencyMs;
     return ::android::OK;
 }
 
diff --git a/audio/aidl/vts/AudioHalBinderServiceUtil.h b/audio/aidl/vts/AudioHalBinderServiceUtil.h
index b4b4632..4ebc1b1 100644
--- a/audio/aidl/vts/AudioHalBinderServiceUtil.h
+++ b/audio/aidl/vts/AudioHalBinderServiceUtil.h
@@ -42,20 +42,9 @@
 
     ndk::SpAIBinder restartService(
             std::chrono::milliseconds timeoutMs = std::chrono::milliseconds(3000)) {
-        mDeathHandler.reset(new AidlDeathRecipient(mBinder));
-        if (STATUS_OK != mDeathHandler->linkToDeath()) {
-            LOG(ERROR) << "linkToDeath failed";
-            return nullptr;
+        if (!stopService(timeoutMs)) {
+            return {};
         }
-        if (!android::base::SetProperty("sys.audio.restart.hal", "1")) {
-            LOG(ERROR) << "SetProperty failed";
-            return nullptr;
-        }
-        if (!mDeathHandler->waitForFired(timeoutMs)) {
-            LOG(ERROR) << "Timeout wait for death";
-            return nullptr;
-        }
-        mDeathHandler.reset();
         return connectToService(mServiceName);
     }
 
@@ -71,8 +60,7 @@
 
         bool waitForFired(std::chrono::milliseconds timeoutMs) {
             std::unique_lock<std::mutex> lock(mutex);
-            condition.wait_for(lock, timeoutMs, [this]() { return fired; });
-            return fired;
+            return condition.wait_for(lock, timeoutMs, [this]() { return fired; });
         }
 
       private:
@@ -94,7 +82,23 @@
         }
     };
 
+    bool stopService(std::chrono::milliseconds timeoutMs) {
+        AidlDeathRecipient deathHandler(mBinder);
+        if (STATUS_OK != deathHandler.linkToDeath()) {
+            LOG(ERROR) << "linkToDeath failed";
+            return false;
+        }
+        if (!android::base::SetProperty("sys.audio.restart.hal", "1")) {
+            LOG(ERROR) << "SetProperty failed";
+            return false;
+        }
+        if (!deathHandler.waitForFired(timeoutMs)) {
+            LOG(ERROR) << "Timeout wait for death of " << mServiceName;
+            return false;
+        }
+        return true;
+    }
+
     std::string mServiceName;
     ndk::SpAIBinder mBinder;
-    std::unique_ptr<AidlDeathRecipient> mDeathHandler;
 };
diff --git a/audio/aidl/vts/ModuleConfig.cpp b/audio/aidl/vts/ModuleConfig.cpp
index 8f19547..a633d83 100644
--- a/audio/aidl/vts/ModuleConfig.cpp
+++ b/audio/aidl/vts/ModuleConfig.cpp
@@ -67,20 +67,7 @@
     return {};
 }
 
-std::vector<aidl::android::media::audio::common::AudioPort>
-ModuleConfig::getAudioPortsForDeviceTypes(const std::vector<AudioDeviceType>& deviceTypes,
-                                          const std::string& connection) {
-    return getAudioPortsForDeviceTypes(mPorts, deviceTypes, connection);
-}
-
 // static
-std::vector<aidl::android::media::audio::common::AudioPort> ModuleConfig::getBuiltInMicPorts(
-        const std::vector<aidl::android::media::audio::common::AudioPort>& ports) {
-    return getAudioPortsForDeviceTypes(
-            ports, std::vector<AudioDeviceType>{AudioDeviceType::IN_MICROPHONE,
-                                                AudioDeviceType::IN_MICROPHONE_BACK});
-}
-
 std::vector<aidl::android::media::audio::common::AudioPort>
 ModuleConfig::getAudioPortsForDeviceTypes(
         const std::vector<aidl::android::media::audio::common::AudioPort>& ports,
@@ -100,6 +87,14 @@
     return result;
 }
 
+// static
+std::vector<aidl::android::media::audio::common::AudioPort> ModuleConfig::getBuiltInMicPorts(
+        const std::vector<aidl::android::media::audio::common::AudioPort>& ports) {
+    return getAudioPortsForDeviceTypes(
+            ports, std::vector<AudioDeviceType>{AudioDeviceType::IN_MICROPHONE,
+                                                AudioDeviceType::IN_MICROPHONE_BACK});
+}
+
 template <typename T>
 auto findById(const std::vector<T>& v, int32_t id) {
     return std::find_if(v.begin(), v.end(), [&](const auto& p) { return p.id == id; });
@@ -119,10 +114,7 @@
             } else {
                 mAttachedSinkDevicePorts.insert(port.id);
             }
-        } else if (devicePort.device.type.connection != AudioDeviceDescription::CONNECTION_VIRTUAL
-                   // The "virtual" connection is used for remote submix which is a dynamic
-                   // device but it can be connected and used w/o external hardware.
-                   && port.profiles.empty()) {
+        } else {
             mExternalDevicePorts.insert(port.id);
         }
     }
@@ -141,6 +133,12 @@
     return result;
 }
 
+std::vector<aidl::android::media::audio::common::AudioPort>
+ModuleConfig::getAudioPortsForDeviceTypes(const std::vector<AudioDeviceType>& deviceTypes,
+                                          const std::string& connection) const {
+    return getAudioPortsForDeviceTypes(mPorts, deviceTypes, connection);
+}
+
 std::vector<AudioPort> ModuleConfig::getConnectedExternalDevicePorts() const {
     std::vector<AudioPort> result;
     std::copy_if(mPorts.begin(), mPorts.end(), std::back_inserter(result), [&](const auto& port) {
@@ -229,6 +227,16 @@
     });
 }
 
+std::vector<AudioPort> ModuleConfig::getRemoteSubmixPorts(bool isInput, bool singlePort) const {
+    AudioDeviceType deviceType = isInput ? AudioDeviceType::IN_SUBMIX : AudioDeviceType::OUT_SUBMIX;
+    auto ports = getAudioPortsForDeviceTypes(std::vector<AudioDeviceType>{deviceType},
+                                             AudioDeviceDescription::CONNECTION_VIRTUAL);
+    if (singlePort) {
+        if (!ports.empty()) ports.resize(1);
+    }
+    return ports;
+}
+
 std::vector<AudioPort> ModuleConfig::getConnectedDevicesPortsForMixPort(
         bool isInput, const AudioPortConfig& mixPortConfig) const {
     const auto mixPortIt = findById<AudioPort>(mPorts, mixPortConfig.portId);
@@ -281,19 +289,29 @@
     return {};
 }
 
-std::vector<AudioPort> ModuleConfig::getRoutableMixPortsForDevicePort(const AudioPort& port) const {
-    std::set<int32_t> portIds;
-    for (const auto& route : mRoutes) {
-        if (port.id == route.sinkPortId) {
-            portIds.insert(route.sourcePortIds.begin(), route.sourcePortIds.end());
-        } else if (auto it = std::find(route.sourcePortIds.begin(), route.sourcePortIds.end(),
-                                       port.id);
-                   it != route.sourcePortIds.end()) {
-            portIds.insert(route.sinkPortId);
-        }
-    }
+std::vector<AudioPort> ModuleConfig::getRoutableDevicePortsForMixPort(const AudioPort& port,
+                                                                      bool connectedOnly) const {
+    std::set<int32_t> portIds = findRoutablePortIds(port.id);
     const bool isInput = port.flags.getTag() == AudioIoFlags::input;
-    return findMixPorts(isInput, false /*connectedOnly*/, false /*singlePort*/,
+    std::set<int32_t> devicePortIds;
+    if (connectedOnly) {
+        devicePortIds = isInput ? getConnectedSourceDevicePorts() : getConnectedSinkDevicePorts();
+    } else {
+        devicePortIds = portIds;
+    }
+    std::vector<AudioPort> result;
+    std::copy_if(mPorts.begin(), mPorts.end(), std::back_inserter(result), [&](const auto& port) {
+        return port.ext.getTag() == AudioPortExt::Tag::device && portIds.count(port.id) > 0 &&
+               devicePortIds.count(port.id) > 0;
+    });
+    return result;
+}
+
+std::vector<AudioPort> ModuleConfig::getRoutableMixPortsForDevicePort(const AudioPort& port,
+                                                                      bool connectedOnly) const {
+    std::set<int32_t> portIds = findRoutablePortIds(port.id);
+    const bool isInput = port.flags.getTag() == AudioIoFlags::input;
+    return findMixPorts(isInput, connectedOnly, false /*singlePort*/,
                         [&portIds](const AudioPort& p) { return portIds.count(p.id) > 0; });
 }
 
@@ -470,6 +488,20 @@
     return result;
 }
 
+std::set<int32_t> ModuleConfig::findRoutablePortIds(int32_t portId) const {
+    std::set<int32_t> portIds;
+    for (const auto& route : mRoutes) {
+        if (portId == route.sinkPortId) {
+            portIds.insert(route.sourcePortIds.begin(), route.sourcePortIds.end());
+        } else if (auto it = std::find(route.sourcePortIds.begin(), route.sourcePortIds.end(),
+                                       portId);
+                   it != route.sourcePortIds.end()) {
+            portIds.insert(route.sinkPortId);
+        }
+    }
+    return portIds;
+}
+
 std::vector<AudioPortConfig> ModuleConfig::generateAudioMixPortConfigs(
         const std::vector<AudioPort>& ports, bool isInput, bool singleProfile) const {
     std::vector<AudioPortConfig> result;
diff --git a/audio/aidl/vts/ModuleConfig.h b/audio/aidl/vts/ModuleConfig.h
index b89adc0..4a87f8c 100644
--- a/audio/aidl/vts/ModuleConfig.h
+++ b/audio/aidl/vts/ModuleConfig.h
@@ -38,9 +38,6 @@
     generateOffloadInfoIfNeeded(
             const aidl::android::media::audio::common::AudioPortConfig& portConfig);
 
-    std::vector<aidl::android::media::audio::common::AudioPort> getAudioPortsForDeviceTypes(
-            const std::vector<aidl::android::media::audio::common::AudioDeviceType>& deviceTypes,
-            const std::string& connection = "");
     static std::vector<aidl::android::media::audio::common::AudioPort> getAudioPortsForDeviceTypes(
             const std::vector<aidl::android::media::audio::common::AudioPort>& ports,
             const std::vector<aidl::android::media::audio::common::AudioDeviceType>& deviceTypes,
@@ -53,6 +50,9 @@
     std::string getError() const { return mStatus.getMessage(); }
 
     std::vector<aidl::android::media::audio::common::AudioPort> getAttachedDevicePorts() const;
+    std::vector<aidl::android::media::audio::common::AudioPort> getAudioPortsForDeviceTypes(
+            const std::vector<aidl::android::media::audio::common::AudioDeviceType>& deviceTypes,
+            const std::string& connection = "") const;
     std::vector<aidl::android::media::audio::common::AudioPort> getConnectedExternalDevicePorts()
             const;
     std::set<int32_t> getConnectedSinkDevicePorts() const;
@@ -85,6 +85,8 @@
     std::vector<aidl::android::media::audio::common::AudioPort> getMmapInMixPorts(
             bool connectedOnly /*Permanently attached and connected external devices*/,
             bool singlePort) const;
+    std::vector<aidl::android::media::audio::common::AudioPort> getRemoteSubmixPorts(
+            bool isInput, bool singlePort) const;
 
     std::vector<aidl::android::media::audio::common::AudioPort> getConnectedDevicesPortsForMixPort(
             bool isInput, const aidl::android::media::audio::common::AudioPort& mixPort) const {
@@ -103,8 +105,12 @@
     std::optional<aidl::android::media::audio::common::AudioPort>
     getSourceMixPortForConnectedDevice() const;
 
+    std::vector<aidl::android::media::audio::common::AudioPort> getRoutableDevicePortsForMixPort(
+            const aidl::android::media::audio::common::AudioPort& port,
+            bool connectedOnly /*Permanently attached and connected external devices*/) const;
     std::vector<aidl::android::media::audio::common::AudioPort> getRoutableMixPortsForDevicePort(
-            const aidl::android::media::audio::common::AudioPort& port) const;
+            const aidl::android::media::audio::common::AudioPort& port,
+            bool connectedOnly /*Permanently attached and connected external devices*/) const;
 
     std::optional<SrcSinkPair> getNonRoutableSrcSinkPair(bool isInput) const;
     std::optional<SrcSinkPair> getRoutableSrcSinkPair(bool isInput) const;
@@ -176,6 +182,7 @@
             bool isInput, bool connectedOnly, bool singlePort,
             const std::function<bool(const aidl::android::media::audio::common::AudioPort&)>& pred)
             const;
+    std::set<int32_t> findRoutablePortIds(int32_t portId) const;
     std::vector<aidl::android::media::audio::common::AudioPortConfig> generateAudioMixPortConfigs(
             const std::vector<aidl::android::media::audio::common::AudioPort>& ports, bool isInput,
             bool singleProfile) const;
diff --git a/audio/aidl/vts/TestUtils.h b/audio/aidl/vts/TestUtils.h
index 191e980..e9f3251 100644
--- a/audio/aidl/vts/TestUtils.h
+++ b/audio/aidl/vts/TestUtils.h
@@ -18,7 +18,6 @@
 
 #include <algorithm>
 #include <initializer_list>
-#include <iostream>
 
 #include <android/binder_auto_utils.h>
 #include <gtest/gtest.h>
@@ -93,4 +92,4 @@
         if ((flags).hwAcceleratorMode == Flags::HardwareAccelerator::TUNNEL || (flags).bypass) { \
             GTEST_SKIP() << "Skip data path for offload";                                        \
         }                                                                                        \
-    })
\ No newline at end of file
+    })
diff --git a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
index 53e51f4..697aae9 100644
--- a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
@@ -113,10 +113,23 @@
 using ndk::ScopedAStatus;
 
 template <typename T>
-auto findById(std::vector<T>& v, int32_t id) {
+std::set<int32_t> extractIds(const std::vector<T>& v) {
+    std::set<int32_t> ids;
+    std::transform(v.begin(), v.end(), std::inserter(ids, ids.begin()),
+                   [](const auto& entity) { return entity.id; });
+    return ids;
+}
+
+template <typename T>
+auto findById(const std::vector<T>& v, int32_t id) {
     return std::find_if(v.begin(), v.end(), [&](const auto& e) { return e.id == id; });
 }
 
+template <typename T>
+auto findAny(const std::vector<T>& v, const std::set<int32_t>& ids) {
+    return std::find_if(v.begin(), v.end(), [&](const auto& e) { return ids.count(e.id) > 0; });
+}
+
 template <typename C>
 std::vector<int32_t> GetNonExistentIds(const C& allIds) {
     if (allIds.empty()) {
@@ -155,8 +168,10 @@
     static int nextId = 0;
     using Tag = AudioDeviceAddress::Tag;
     const auto& deviceDescription = port.ext.get<AudioPortExt::Tag::device>().device.type;
-    AudioDeviceAddress address;
-    if (kPointToPointConnections.count(deviceDescription.connection) == 0) {
+    AudioDeviceAddress address = port.ext.get<AudioPortExt::Tag::device>().device.address;
+    // If the address is already set, do not re-generate.
+    if (address == AudioDeviceAddress() &&
+        kPointToPointConnections.count(deviceDescription.connection) == 0) {
         switch (suggestDeviceAddressTag(deviceDescription)) {
             case Tag::id:
                 address = AudioDeviceAddress::make<Tag::id>(std::to_string(++nextId));
@@ -417,18 +432,21 @@
 // Can be used as a base for any test here, does not depend on the fixture GTest parameters.
 class AudioCoreModuleBase {
   public:
-    // Default buffer sizes are used mostly for negative tests.
-    static constexpr int kDefaultBufferSizeFrames = 256;
+    // Fixed buffer size are used for negative tests only. For any tests involving stream
+    // opening that must success, the minimum buffer size must be obtained from a patch.
+    // This is implemented by the 'StreamFixture' utility class.
+    static constexpr int kNegativeTestBufferSizeFrames = 256;
     static constexpr int kDefaultLargeBufferSizeFrames = 48000;
 
-    void SetUpImpl(const std::string& moduleName) {
-        ASSERT_NO_FATAL_FAILURE(ConnectToService(moduleName));
+    void SetUpImpl(const std::string& moduleName, bool setUpDebug = true) {
+        ASSERT_NO_FATAL_FAILURE(ConnectToService(moduleName, setUpDebug));
         ASSERT_IS_OK(module->getAudioPorts(&initialPorts));
         ASSERT_IS_OK(module->getAudioRoutes(&initialRoutes));
     }
 
     void TearDownImpl() {
         debug.reset();
+        ASSERT_NE(module, nullptr);
         std::vector<AudioPort> finalPorts;
         ASSERT_IS_OK(module->getAudioPorts(&finalPorts));
         EXPECT_NO_FATAL_FAILURE(VerifyVectorsAreEqual<AudioPort>(initialPorts, finalPorts))
@@ -439,21 +457,26 @@
                 << "The list of audio routes was not restored to the initial state";
     }
 
-    void ConnectToService(const std::string& moduleName) {
+    void ConnectToService(const std::string& moduleName, bool setUpDebug) {
         ASSERT_EQ(module, nullptr);
         ASSERT_EQ(debug, nullptr);
         module = IModule::fromBinder(binderUtil.connectToService(moduleName));
         ASSERT_NE(module, nullptr);
-        ASSERT_NO_FATAL_FAILURE(SetUpDebug());
+        if (setUpDebug) {
+            ASSERT_NO_FATAL_FAILURE(SetUpDebug());
+        }
     }
 
     void RestartService() {
         ASSERT_NE(module, nullptr);
         moduleConfig.reset();
+        const bool setUpDebug = !!debug;
         debug.reset();
         module = IModule::fromBinder(binderUtil.restartService());
         ASSERT_NE(module, nullptr);
-        ASSERT_NO_FATAL_FAILURE(SetUpDebug());
+        if (setUpDebug) {
+            ASSERT_NO_FATAL_FAILURE(SetUpDebug());
+        }
     }
 
     void SetUpDebug() {
@@ -493,9 +516,7 @@
                          const std::string& errorMessage) {
         std::vector<Entity> entities;
         { ASSERT_IS_OK((module.get()->*getter)(&entities)); }
-        std::transform(entities.begin(), entities.end(),
-                       std::inserter(*entityIds, entityIds->begin()),
-                       [](const auto& entity) { return entity.id; });
+        *entityIds = extractIds<Entity>(entities);
         EXPECT_EQ(entities.size(), entityIds->size()) << errorMessage;
     }
 
@@ -1112,6 +1133,7 @@
 template <typename T>
 struct IOTraits {
     static constexpr bool is_input = std::is_same_v<T, IStreamIn>;
+    static constexpr const char* directionStr = is_input ? "input" : "output";
     using Worker = std::conditional_t<is_input, StreamReader, StreamWriter>;
 };
 
@@ -1143,8 +1165,7 @@
     }
     ScopedAStatus SetUpNoChecks(IModule* module, const AudioPortConfig& portConfig,
                                 long bufferSizeFrames);
-    void SetUp(IModule* module, long bufferSizeFrames) {
-        ASSERT_NO_FATAL_FAILURE(SetUpPortConfig(module));
+    void SetUpStream(IModule* module, long bufferSizeFrames) {
         ASSERT_IS_OK(SetUpNoChecks(module, bufferSizeFrames)) << "port config id " << getPortId();
         ASSERT_NE(nullptr, mStream) << "port config id " << getPortId();
         EXPECT_GE(mDescriptor.bufferSizeFrames, bufferSizeFrames)
@@ -1152,6 +1173,10 @@
         mContext.emplace(mDescriptor);
         ASSERT_NO_FATAL_FAILURE(mContext.value().checkIsValid());
     }
+    void SetUp(IModule* module, long bufferSizeFrames) {
+        ASSERT_NO_FATAL_FAILURE(SetUpPortConfig(module));
+        ASSERT_NO_FATAL_FAILURE(SetUpStream(module, bufferSizeFrames));
+    }
     Stream* get() const { return mStream.get(); }
     const StreamContext* getContext() const { return mContext ? &(mContext.value()) : nullptr; }
     StreamEventReceiver* getEventReceiver() { return mStreamCallback->getEventReceiver(); }
@@ -1291,6 +1316,9 @@
     }
     int32_t getId() const { return mPatch.id; }
     const AudioPatch& get() const { return mPatch; }
+    int32_t getMinimumStreamBufferSizeFrames() const {
+        return mPatch.minimumStreamBufferSizeFrames;
+    }
     const AudioPortConfig& getSinkPortConfig() const { return mSinkPortConfig.get(); }
     const AudioPortConfig& getSrcPortConfig() const { return mSrcPortConfig.get(); }
     const AudioPortConfig& getPortConfig(bool getSink) const {
@@ -1503,8 +1531,8 @@
         EXPECT_EQ(portConnected.get(), connectedPort);
         const auto& portProfiles = connectedPort.profiles;
         if (portProfiles.empty()) {
-            const auto routableMixPorts =
-                    moduleConfig->getRoutableMixPortsForDevicePort(connectedPort);
+            const auto routableMixPorts = moduleConfig->getRoutableMixPortsForDevicePort(
+                    connectedPort, true /*connectedOnly*/);
             bool hasMixPortWithStaticProfile = false;
             for (const auto& mixPort : routableMixPorts) {
                 const auto& mixPortProfiles = mixPort.profiles;
@@ -1545,7 +1573,7 @@
         {
             aidl::android::hardware::audio::core::IModule::OpenInputStreamArguments args;
             args.portConfigId = portConfigId;
-            args.bufferSizeFrames = kDefaultBufferSizeFrames;
+            args.bufferSizeFrames = kNegativeTestBufferSizeFrames;
             aidl::android::hardware::audio::core::IModule::OpenInputStreamReturn ret;
             EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, module->openInputStream(args, &ret))
                     << "port config ID " << portConfigId;
@@ -1554,7 +1582,7 @@
         {
             aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args;
             args.portConfigId = portConfigId;
-            args.bufferSizeFrames = kDefaultBufferSizeFrames;
+            args.bufferSizeFrames = kNegativeTestBufferSizeFrames;
             aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret;
             EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, module->openOutputStream(args, &ret))
                     << "port config ID " << portConfigId;
@@ -1717,6 +1745,10 @@
     doNotSimulateConnections.flags().simulateDeviceConnections = false;
     ASSERT_NO_FATAL_FAILURE(doNotSimulateConnections.SetUp(module.get()));
     for (const auto& port : ports) {
+        // Virtual devices may not require external hardware and thus can always be connected.
+        if (port.ext.get<AudioPortExt::device>().device.type.connection ==
+            AudioDeviceDescription::CONNECTION_VIRTUAL)
+            continue;
         AudioPort portWithData = GenerateUniqueDeviceAddress(port), connectedPort;
         ScopedAStatus status = module->connectExternalDevice(portWithData, &connectedPort);
         EXPECT_STATUS(EX_ILLEGAL_STATE, status) << "static port " << portWithData.toString();
@@ -2563,6 +2595,260 @@
     std::vector<std::string> mStatuses;
 };
 
+// A helper which sets up necessary HAL structures for a proper stream initialization.
+//
+// The full sequence of actions to set up a stream is as follows:
+//
+//  device port -> connect if necessary -> set up port config   | -> set up patch
+//  mix port -> set up port config, unless it has been provided |
+//
+//  then, from the patch, figure out the minimum HAL buffer size -> set up stream
+//
+// This sequence is reflected in the order of fields declaration.
+// Various tests need to be able to start and stop at various point in this sequence,
+// this is why there are methods that do just part of the work.
+//
+// Note: To maximize test coverage, this class relies on simulation of external device
+// connections by the HAL module.
+template <typename Stream>
+class StreamFixture {
+  public:
+    // Tests might need to override the direction.
+    StreamFixture(bool isInput = IOTraits<Stream>::is_input) : mIsInput(isInput) {}
+
+    void SetUpPortConfigAnyMixPort(IModule* module, ModuleConfig* moduleConfig,
+                                   bool connectedOnly) {
+        const auto mixPorts = moduleConfig->getMixPorts(mIsInput, connectedOnly);
+        mSkipTestReason = "No mix ports";
+        for (const auto& mixPort : mixPorts) {
+            mSkipTestReason = "";
+            ASSERT_NO_FATAL_FAILURE(SetUpPortConfigForMixPortOrConfig(module, moduleConfig, mixPort,
+                                                                      connectedOnly));
+            if (mSkipTestReason.empty()) break;
+        }
+    }
+
+    void SetUpPortConfigForMixPortOrConfig(
+            IModule* module, ModuleConfig* moduleConfig, const AudioPort& initialMixPort,
+            bool connectedOnly, const std::optional<AudioPortConfig>& mixPortConfig = {}) {
+        if (mixPortConfig.has_value() && !connectedOnly) {
+            // Connecting an external device may cause change in mix port profiles and the provided
+            // config may become invalid.
+            LOG(FATAL) << __func__ << ": when specifying a mix port config, it is not allowed "
+                       << "to change connected devices, thus `connectedOnly` must be `true`";
+        }
+        std::optional<AudioPort> connectedDevicePort;
+        ASSERT_NO_FATAL_FAILURE(SetUpDevicePortForMixPort(module, moduleConfig, initialMixPort,
+                                                          connectedOnly, &connectedDevicePort));
+        if (!mSkipTestReason.empty()) return;
+        if (mixPortConfig.has_value()) {
+            ASSERT_NO_FATAL_FAILURE(
+                    SetUpPortConfig(module, moduleConfig, *mixPortConfig, *connectedDevicePort));
+        } else {
+            // If an external device was connected, the profiles of the mix port might have changed.
+            AudioPort mixPort;
+            ASSERT_NO_FATAL_FAILURE(module->getAudioPort(initialMixPort.id, &mixPort));
+            ASSERT_NO_FATAL_FAILURE(
+                    SetUpPortConfig(module, moduleConfig, mixPort, *connectedDevicePort));
+        }
+    }
+
+    void SetUpPortConfig(IModule* module, ModuleConfig* moduleConfig, const AudioPort& mixPort,
+                         const AudioPort& devicePort) {
+        auto mixPortConfig = moduleConfig->getSingleConfigForMixPort(mIsInput, mixPort);
+        ASSERT_TRUE(mixPortConfig.has_value())
+                << "Unable to generate port config for mix port " << mixPort.toString();
+        ASSERT_NO_FATAL_FAILURE(SetUpPortConfig(module, moduleConfig, *mixPortConfig, devicePort));
+    }
+    void SetUpPortConfig(IModule* module, ModuleConfig* moduleConfig,
+                         const AudioPortConfig& mixPortConfig, const AudioPort& devicePort) {
+        ASSERT_NO_FATAL_FAILURE(SetUpPatch(module, moduleConfig, mixPortConfig, devicePort));
+        mStream = std::make_unique<WithStream<Stream>>(mMixPortConfig->get());
+        ASSERT_NO_FATAL_FAILURE(mStream->SetUpPortConfig(module));
+    }
+
+    ScopedAStatus SetUpStreamNoChecks(IModule* module) {
+        return mStream->SetUpNoChecks(module, getMinimumStreamBufferSizeFrames());
+    }
+    void SetUpStream(IModule* module) {
+        ASSERT_NO_FATAL_FAILURE(mStream->SetUpStream(module, getMinimumStreamBufferSizeFrames()));
+    }
+
+    void SetUpStreamForDevicePort(IModule* module, ModuleConfig* moduleConfig,
+                                  const AudioPort& devicePort, bool connectedOnly = false) {
+        ASSERT_NO_FATAL_FAILURE(
+                SetUpPortConfigForDevicePort(module, moduleConfig, devicePort, connectedOnly));
+        if (!mSkipTestReason.empty()) return;
+        ASSERT_NO_FATAL_FAILURE(SetUpStream(module));
+    }
+    void SetUpStreamForAnyMixPort(IModule* module, ModuleConfig* moduleConfig,
+                                  bool connectedOnly = false) {
+        ASSERT_NO_FATAL_FAILURE(SetUpPortConfigAnyMixPort(module, moduleConfig, connectedOnly));
+        if (!mSkipTestReason.empty()) return;
+        ASSERT_NO_FATAL_FAILURE(SetUpStream(module));
+    }
+    void SetUpStreamForMixPort(IModule* module, ModuleConfig* moduleConfig,
+                               const AudioPort& mixPort, bool connectedOnly = false) {
+        ASSERT_NO_FATAL_FAILURE(
+                SetUpPortConfigForMixPortOrConfig(module, moduleConfig, mixPort, connectedOnly));
+        if (!mSkipTestReason.empty()) return;
+        ASSERT_NO_FATAL_FAILURE(SetUpStream(module));
+    }
+    void SetUpStreamForPortsPair(IModule* module, ModuleConfig* moduleConfig,
+                                 const AudioPort& mixPort, const AudioPort& devicePort) {
+        ASSERT_NO_FATAL_FAILURE(SetUpPortConfig(module, moduleConfig, mixPort, devicePort));
+        if (!mSkipTestReason.empty()) return;
+        ASSERT_NO_FATAL_FAILURE(SetUpStream(module));
+    }
+    void SetUpStreamForMixPortConfig(IModule* module, ModuleConfig* moduleConfig,
+                                     const AudioPortConfig& mixPortConfig) {
+        // Since mix port configs may change after connecting an external device,
+        // only connected device ports are considered.
+        constexpr bool connectedOnly = true;
+        const auto& ports = moduleConfig->getMixPorts(mIsInput, connectedOnly);
+        const auto mixPortIt = findById<AudioPort>(ports, mixPortConfig.portId);
+        ASSERT_NE(mixPortIt, ports.end()) << "Port id " << mixPortConfig.portId << " not found";
+        ASSERT_NO_FATAL_FAILURE(SetUpPortConfigForMixPortOrConfig(module, moduleConfig, *mixPortIt,
+                                                                  connectedOnly, mixPortConfig));
+        if (!mSkipTestReason.empty()) return;
+        ASSERT_NO_FATAL_FAILURE(SetUpStream(module));
+    }
+    void SetUpPatchForMixPortConfig(IModule* module, ModuleConfig* moduleConfig,
+                                    const AudioPortConfig& mixPortConfig) {
+        constexpr bool connectedOnly = true;
+        const auto& ports = moduleConfig->getMixPorts(mIsInput, connectedOnly);
+        const auto mixPortIt = findById<AudioPort>(ports, mixPortConfig.portId);
+        ASSERT_NE(mixPortIt, ports.end()) << "Port id " << mixPortConfig.portId << " not found";
+        std::optional<AudioPort> connectedDevicePort;
+        ASSERT_NO_FATAL_FAILURE(SetUpDevicePortForMixPort(module, moduleConfig, *mixPortIt,
+                                                          connectedOnly, &connectedDevicePort));
+        if (!mSkipTestReason.empty()) return;
+        ASSERT_NO_FATAL_FAILURE(
+                SetUpPatch(module, moduleConfig, mixPortConfig, *connectedDevicePort));
+    }
+
+    void ReconnectPatch(IModule* module) {
+        mPatch = std::make_unique<WithAudioPatch>(mIsInput, mMixPortConfig->get(),
+                                                  mDevicePortConfig->get());
+        ASSERT_NO_FATAL_FAILURE(mPatch->SetUp(module));
+    }
+    void TeardownPatch() { mPatch.reset(); }
+    // Assuming that the patch is set up, while the stream isn't yet,
+    // tear the patch down and set up stream.
+    void TeardownPatchSetUpStream(IModule* module) {
+        const int32_t bufferSize = getMinimumStreamBufferSizeFrames();
+        ASSERT_NO_FATAL_FAILURE(TeardownPatch());
+        mStream = std::make_unique<WithStream<Stream>>(mMixPortConfig->get());
+        ASSERT_NO_FATAL_FAILURE(mStream->SetUpPortConfig(module));
+        ASSERT_NO_FATAL_FAILURE(mStream->SetUpStream(module, bufferSize));
+    }
+
+    const AudioDevice& getDevice() const { return mDevice; }
+    int32_t getMinimumStreamBufferSizeFrames() const {
+        return mPatch->getMinimumStreamBufferSizeFrames();
+    }
+    const AudioPatch& getPatch() const { return mPatch->get(); }
+    const AudioPortConfig& getPortConfig() const { return mMixPortConfig->get(); }
+    int32_t getPortId() const { return mMixPortConfig->getId(); }
+    Stream* getStream() const { return mStream->get(); }
+    const StreamContext* getStreamContext() const { return mStream->getContext(); }
+    StreamEventReceiver* getStreamEventReceiver() { return mStream->getEventReceiver(); }
+    std::shared_ptr<Stream> getStreamSharedPointer() const { return mStream->getSharedPointer(); }
+    const std::string& skipTestReason() const { return mSkipTestReason; }
+
+  private:
+    void SetUpDevicePort(IModule* module, ModuleConfig* moduleConfig,
+                         const std::set<int32_t>& devicePortIds, bool connectedOnly,
+                         std::optional<AudioPort>* connectedDevicePort) {
+        const auto attachedDevicePorts = moduleConfig->getAttachedDevicePorts();
+        if (auto it = findAny<AudioPort>(attachedDevicePorts, devicePortIds);
+            it != attachedDevicePorts.end()) {
+            *connectedDevicePort = *it;
+            LOG(DEBUG) << __func__ << ": found attached port " << it->toString();
+        }
+        const auto connectedDevicePorts = moduleConfig->getConnectedExternalDevicePorts();
+        if (auto it = findAny<AudioPort>(connectedDevicePorts, devicePortIds);
+            it != connectedDevicePorts.end()) {
+            *connectedDevicePort = *it;
+            LOG(DEBUG) << __func__ << ": found connected port " << it->toString();
+        }
+        if (!connectedOnly && !connectedDevicePort->has_value()) {
+            const auto externalDevicePorts = moduleConfig->getExternalDevicePorts();
+            if (auto it = findAny<AudioPort>(externalDevicePorts, devicePortIds);
+                it != externalDevicePorts.end()) {
+                AudioPort portWithData = GenerateUniqueDeviceAddress(*it);
+                mPortConnected = std::make_unique<WithDevicePortConnectedState>(portWithData);
+                ASSERT_NO_FATAL_FAILURE(mPortConnected->SetUp(module, moduleConfig));
+                *connectedDevicePort = mPortConnected->get();
+                LOG(DEBUG) << __func__ << ": connected port " << mPortConnected->get().toString();
+            }
+        }
+    }
+    void SetUpDevicePortForMixPort(IModule* module, ModuleConfig* moduleConfig,
+                                   const AudioPort& mixPort, bool connectedOnly,
+                                   std::optional<AudioPort>* connectedDevicePort) {
+        const auto devicePorts =
+                moduleConfig->getRoutableDevicePortsForMixPort(mixPort, connectedOnly);
+        if (devicePorts.empty()) {
+            mSkipTestReason = std::string("No routable device ports found for mix port id ")
+                                      .append(std::to_string(mixPort.id));
+            LOG(DEBUG) << __func__ << ": " << mSkipTestReason;
+            return;
+        };
+        ASSERT_NO_FATAL_FAILURE(SetUpDevicePort(module, moduleConfig,
+                                                extractIds<AudioPort>(devicePorts), connectedOnly,
+                                                connectedDevicePort));
+        if (!connectedDevicePort->has_value()) {
+            mSkipTestReason = std::string("Unable to find a device port pair for mix port id ")
+                                      .append(std::to_string(mixPort.id));
+            LOG(DEBUG) << __func__ << ": " << mSkipTestReason;
+            return;
+        }
+    }
+    void SetUpPortConfigForDevicePort(IModule* module, ModuleConfig* moduleConfig,
+                                      const AudioPort& devicePort, bool connectedOnly) {
+        std::optional<AudioPort> connectedDevicePort;
+        ASSERT_NO_FATAL_FAILURE(SetUpDevicePort(module, moduleConfig, {devicePort.id},
+                                                connectedOnly, &connectedDevicePort));
+        if (!connectedDevicePort.has_value()) {
+            mSkipTestReason = std::string("Device port id ")
+                                      .append(std::to_string(devicePort.id))
+                                      .append(" is not attached and can not be connected");
+            return;
+        }
+        const auto mixPorts = moduleConfig->getRoutableMixPortsForDevicePort(
+                *connectedDevicePort, true /*connectedOnly*/);
+        if (mixPorts.empty()) {
+            mSkipTestReason = std::string("No routable mix ports found for device port id ")
+                                      .append(std::to_string(devicePort.id));
+            return;
+        }
+        ASSERT_NO_FATAL_FAILURE(
+                SetUpPortConfig(module, moduleConfig, *mixPorts.begin(), *connectedDevicePort));
+    }
+    void SetUpPatch(IModule* module, ModuleConfig* moduleConfig,
+                    const AudioPortConfig& mixPortConfig, const AudioPort& devicePort) {
+        mMixPortConfig = std::make_unique<WithAudioPortConfig>(mixPortConfig);
+        ASSERT_NO_FATAL_FAILURE(mMixPortConfig->SetUp(module));
+        mDevicePortConfig = std::make_unique<WithAudioPortConfig>(
+                moduleConfig->getSingleConfigForDevicePort(devicePort));
+        ASSERT_NO_FATAL_FAILURE(mDevicePortConfig->SetUp(module));
+        mDevice = devicePort.ext.get<AudioPortExt::device>().device;
+        mPatch = std::make_unique<WithAudioPatch>(mIsInput, mMixPortConfig->get(),
+                                                  mDevicePortConfig->get());
+        ASSERT_NO_FATAL_FAILURE(mPatch->SetUp(module));
+    }
+
+    const bool mIsInput;
+    std::string mSkipTestReason;
+    std::unique_ptr<WithDevicePortConnectedState> mPortConnected;
+    AudioDevice mDevice;
+    std::unique_ptr<WithAudioPortConfig> mMixPortConfig;
+    std::unique_ptr<WithAudioPortConfig> mDevicePortConfig;
+    std::unique_ptr<WithAudioPatch> mPatch;
+    std::unique_ptr<WithStream<Stream>> mStream;
+};
+
 template <typename Stream>
 class AudioStream : public AudioCoreModule {
   public:
@@ -2572,16 +2858,15 @@
     }
 
     void GetStreamCommon() {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            GTEST_SKIP() << "No mix port for attached devices";
+        StreamFixture<Stream> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForAnyMixPort(module.get(), moduleConfig.get()));
+        if (auto reason = stream.skipTestReason(); !reason.empty()) {
+            GTEST_SKIP() << reason;
         }
-        WithStream<Stream> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
         std::shared_ptr<IStreamCommon> streamCommon1;
-        EXPECT_IS_OK(stream.get()->getStreamCommon(&streamCommon1));
+        EXPECT_IS_OK(stream.getStream()->getStreamCommon(&streamCommon1));
         std::shared_ptr<IStreamCommon> streamCommon2;
-        EXPECT_IS_OK(stream.get()->getStreamCommon(&streamCommon2));
+        EXPECT_IS_OK(stream.getStream()->getStreamCommon(&streamCommon2));
         ASSERT_NE(nullptr, streamCommon1);
         ASSERT_NE(nullptr, streamCommon2);
         EXPECT_EQ(streamCommon1->asBinder(), streamCommon2->asBinder())
@@ -2589,31 +2874,31 @@
     }
 
     void CloseTwice() {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            GTEST_SKIP() << "No mix port for attached devices";
-        }
         std::shared_ptr<Stream> heldStream;
         {
-            WithStream<Stream> stream(portConfig.value());
-            ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
-            heldStream = stream.getSharedPointer();
+            StreamFixture<Stream> stream;
+            ASSERT_NO_FATAL_FAILURE(
+                    stream.SetUpStreamForAnyMixPort(module.get(), moduleConfig.get()));
+            if (auto reason = stream.skipTestReason(); !reason.empty()) {
+                GTEST_SKIP() << reason;
+            }
+            heldStream = stream.getStreamSharedPointer();
         }
         EXPECT_STATUS(EX_ILLEGAL_STATE, WithStream<Stream>::callClose(heldStream))
                 << "when closing the stream twice";
     }
 
     void PrepareToCloseTwice() {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            GTEST_SKIP() << "No mix port for attached devices";
-        }
         std::shared_ptr<IStreamCommon> heldStreamCommon;
         {
-            WithStream<Stream> stream(portConfig.value());
-            ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+            StreamFixture<Stream> stream;
+            ASSERT_NO_FATAL_FAILURE(
+                    stream.SetUpStreamForAnyMixPort(module.get(), moduleConfig.get()));
+            if (auto reason = stream.skipTestReason(); !reason.empty()) {
+                GTEST_SKIP() << reason;
+            }
             std::shared_ptr<IStreamCommon> streamCommon;
-            ASSERT_IS_OK(stream.get()->getStreamCommon(&streamCommon));
+            ASSERT_IS_OK(stream.getStream()->getStreamCommon(&streamCommon));
             heldStreamCommon = streamCommon;
             EXPECT_IS_OK(streamCommon->prepareToClose());
             EXPECT_IS_OK(streamCommon->prepareToClose())
@@ -2626,9 +2911,13 @@
     void OpenAllConfigs() {
         const auto allPortConfigs =
                 moduleConfig->getPortConfigsForMixPorts(IOTraits<Stream>::is_input);
+        if (allPortConfigs.empty()) {
+            GTEST_SKIP() << "No mix ports for attached devices";
+        }
         for (const auto& portConfig : allPortConfigs) {
-            WithStream<Stream> stream(portConfig);
-            ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+            StreamFixture<Stream> stream;
+            ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPortConfig(
+                    module.get(), moduleConfig.get(), portConfig));
         }
     }
 
@@ -2648,22 +2937,21 @@
 
     void OpenInvalidDirection() {
         // Important! The direction of the port config must be reversed.
-        const auto portConfig =
-                moduleConfig->getSingleConfigForMixPort(!IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            GTEST_SKIP() << "No mix port for attached devices";
+        StreamFixture<Stream> stream(!IOTraits<Stream>::is_input);
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfigAnyMixPort(module.get(), moduleConfig.get(),
+                                                                 false /*connectedOnly*/));
+        if (auto reason = stream.skipTestReason(); !reason.empty()) {
+            GTEST_SKIP() << reason;
         }
-        WithStream<Stream> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get()));
-        EXPECT_STATUS(EX_ILLEGAL_ARGUMENT,
-                      stream.SetUpNoChecks(module.get(), kDefaultBufferSizeFrames))
+        EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, stream.SetUpStreamNoChecks(module.get()))
                 << "port config ID " << stream.getPortId();
-        EXPECT_EQ(nullptr, stream.get());
+        EXPECT_EQ(nullptr, stream.getStream());
     }
 
     void OpenOverMaxCount() {
+        constexpr bool connectedOnly = true;
         constexpr bool isInput = IOTraits<Stream>::is_input;
-        auto ports = moduleConfig->getMixPorts(isInput, true /*connectedOnly*/);
+        auto ports = moduleConfig->getMixPorts(isInput, connectedOnly);
         bool hasSingleRun = false;
         for (const auto& port : ports) {
             const size_t maxStreamCount = port.ext.get<AudioPortExt::Tag::mix>().maxOpenStreamCount;
@@ -2676,16 +2964,16 @@
                 continue;
             }
             hasSingleRun = true;
-            std::optional<WithStream<Stream>> streamWraps[maxStreamCount + 1];
+            StreamFixture<Stream> streams[maxStreamCount + 1];
             for (size_t i = 0; i <= maxStreamCount; ++i) {
-                streamWraps[i].emplace(portConfigs[i]);
-                WithStream<Stream>& stream = streamWraps[i].value();
+                ASSERT_NO_FATAL_FAILURE(streams[i].SetUpPortConfigForMixPortOrConfig(
+                        module.get(), moduleConfig.get(), port, connectedOnly, portConfigs[i]));
+                ASSERT_EQ("", streams[i].skipTestReason());
+                auto& stream = streams[i];
                 if (i < maxStreamCount) {
-                    ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+                    ASSERT_NO_FATAL_FAILURE(stream.SetUpStream(module.get()));
                 } else {
-                    ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get()));
-                    EXPECT_STATUS(EX_ILLEGAL_STATE,
-                                  stream.SetUpNoChecks(module.get(), kDefaultBufferSizeFrames))
+                    EXPECT_STATUS(EX_ILLEGAL_STATE, stream.SetUpStreamNoChecks(module.get()))
                             << "port config ID " << stream.getPortId() << ", maxOpenStreamCount is "
                             << maxStreamCount;
                 }
@@ -2705,12 +2993,11 @@
     }
 
     void ResetPortConfigWithOpenStream() {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            GTEST_SKIP() << "No mix port for attached devices";
+        StreamFixture<Stream> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForAnyMixPort(module.get(), moduleConfig.get()));
+        if (auto reason = stream.skipTestReason(); !reason.empty()) {
+            GTEST_SKIP() << reason;
         }
-        WithStream<Stream> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
         EXPECT_STATUS(EX_ILLEGAL_STATE, module->resetAudioPortConfig(stream.getPortId()))
                 << "port config ID " << stream.getPortId();
     }
@@ -2724,14 +3011,13 @@
     }
 
     void UpdateHwAvSyncId() {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            GTEST_SKIP() << "No mix port for attached devices";
+        StreamFixture<Stream> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForAnyMixPort(module.get(), moduleConfig.get()));
+        if (auto reason = stream.skipTestReason(); !reason.empty()) {
+            GTEST_SKIP() << reason;
         }
-        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_IS_OK(stream.getStream()->getStreamCommon(&streamCommon));
         ASSERT_NE(nullptr, streamCommon);
         const auto kStatuses = {EX_NONE, EX_ILLEGAL_ARGUMENT, EX_ILLEGAL_STATE};
         for (const auto id : {-100, -1, 0, 1, 100}) {
@@ -2744,14 +3030,13 @@
     }
 
     void GetVendorParameters() {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            GTEST_SKIP() << "No mix port for attached devices";
+        StreamFixture<Stream> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForAnyMixPort(module.get(), moduleConfig.get()));
+        if (auto reason = stream.skipTestReason(); !reason.empty()) {
+            GTEST_SKIP() << reason;
         }
-        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_IS_OK(stream.getStream()->getStreamCommon(&streamCommon));
         ASSERT_NE(nullptr, streamCommon);
 
         bool isGetterSupported = false;
@@ -2765,14 +3050,13 @@
     }
 
     void SetVendorParameters() {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            GTEST_SKIP() << "No mix port for attached devices";
+        StreamFixture<Stream> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForAnyMixPort(module.get(), moduleConfig.get()));
+        if (auto reason = stream.skipTestReason(); !reason.empty()) {
+            GTEST_SKIP() << reason;
         }
-        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_IS_OK(stream.getStream()->getStreamCommon(&streamCommon));
         ASSERT_NE(nullptr, streamCommon);
 
         bool isSupported = false;
@@ -2783,32 +3067,37 @@
     }
 
     void HwGainHwVolume() {
-        const auto ports =
-                moduleConfig->getMixPorts(IOTraits<Stream>::is_input, true /*connectedOnly*/);
+        // Since device connection emulation does not cover complete functionality,
+        // only use this test with connected devices.
+        constexpr bool connectedOnly = true;
+        const auto ports = moduleConfig->getMixPorts(IOTraits<Stream>::is_input, connectedOnly);
         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));
+            SCOPED_TRACE(port.toString());
+            StreamFixture<Stream> stream;
+            ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPort(module.get(), moduleConfig.get(),
+                                                                 port, connectedOnly));
+            if (!stream.skipTestReason().empty()) continue;
+            const auto portConfig = stream.getPortConfig();
+            SCOPED_TRACE(portConfig.toString());
             std::vector<std::vector<float>> validValues, invalidValues;
             bool isSupported = false;
             if constexpr (IOTraits<Stream>::is_input) {
-                GenerateTestArrays<float>(getChannelCount(portConfig.value().channelMask.value()),
+                GenerateTestArrays<float>(getChannelCount(portConfig.channelMask.value()),
                                           IStreamIn::HW_GAIN_MIN, IStreamIn::HW_GAIN_MAX,
                                           &validValues, &invalidValues);
                 EXPECT_NO_FATAL_FAILURE(TestAccessors<std::vector<float>>(
-                        stream.get(), &IStreamIn::getHwGain, &IStreamIn::setHwGain, validValues,
-                        invalidValues, &isSupported));
+                        stream.getStream(), &IStreamIn::getHwGain, &IStreamIn::setHwGain,
+                        validValues, invalidValues, &isSupported));
             } else {
-                GenerateTestArrays<float>(getChannelCount(portConfig.value().channelMask.value()),
+                GenerateTestArrays<float>(getChannelCount(portConfig.channelMask.value()),
                                           IStreamOut::HW_VOLUME_MIN, IStreamOut::HW_VOLUME_MAX,
                                           &validValues, &invalidValues);
                 EXPECT_NO_FATAL_FAILURE(TestAccessors<std::vector<float>>(
-                        stream.get(), &IStreamOut::getHwVolume, &IStreamOut::setHwVolume,
+                        stream.getStream(), &IStreamOut::getHwVolume, &IStreamOut::setHwVolume,
                         validValues, invalidValues, &isSupported));
             }
             if (isSupported) atLeastOneSupports = true;
@@ -2822,19 +3111,22 @@
     // 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, true /*connectedOnly*/);
+        constexpr bool connectedOnly = true;
+        const auto ports = moduleConfig->getMixPorts(IOTraits<Stream>::is_input, connectedOnly);
         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));
+            SCOPED_TRACE(port.toString());
+            StreamFixture<Stream> stream;
+            ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPort(module.get(), moduleConfig.get(),
+                                                                 port, connectedOnly));
+            if (!stream.skipTestReason().empty()) continue;
+            const auto portConfig = stream.getPortConfig();
+            SCOPED_TRACE(portConfig.toString());
             std::shared_ptr<IStreamCommon> streamCommon;
-            ASSERT_IS_OK(stream.get()->getStreamCommon(&streamCommon));
+            ASSERT_IS_OK(stream.getStream()->getStreamCommon(&streamCommon));
             ASSERT_NE(nullptr, streamCommon);
             ndk::ScopedAStatus addEffectStatus = streamCommon->addEffect(nullptr);
             ndk::ScopedAStatus removeEffectStatus = streamCommon->removeEffect(nullptr);
@@ -2854,11 +3146,14 @@
     }
 
     void OpenTwiceSamePortConfigImpl(const AudioPortConfig& portConfig) {
-        WithStream<Stream> stream1(portConfig);
-        ASSERT_NO_FATAL_FAILURE(stream1.SetUp(module.get(), kDefaultBufferSizeFrames));
+        StreamFixture<Stream> stream1;
+        ASSERT_NO_FATAL_FAILURE(
+                stream1.SetUpStreamForMixPortConfig(module.get(), moduleConfig.get(), portConfig));
+        ASSERT_EQ("", stream1.skipTestReason());
         WithStream<Stream> stream2;
-        EXPECT_STATUS(EX_ILLEGAL_STATE, stream2.SetUpNoChecks(module.get(), stream1.getPortConfig(),
-                                                              kDefaultBufferSizeFrames))
+        EXPECT_STATUS(EX_ILLEGAL_STATE,
+                      stream2.SetUpNoChecks(module.get(), stream1.getPortConfig(),
+                                            stream1.getMinimumStreamBufferSizeFrames()))
                 << "when opening a stream twice for the same port config ID "
                 << stream1.getPortId();
     }
@@ -2893,11 +3188,13 @@
         for (const auto& seq : sequences) {
             SCOPED_TRACE(std::string("Sequence ").append(seq.first));
             LOG(DEBUG) << __func__ << ": Sequence " << seq.first;
-            WithStream<Stream> stream(portConfig);
-            ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+            StreamFixture<Stream> stream;
+            ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPortConfig(
+                    module.get(), moduleConfig.get(), portConfig));
+            ASSERT_EQ("", stream.skipTestReason());
             StreamLogicDriverInvalidCommand driver(seq.second);
-            typename IOTraits<Stream>::Worker worker(*stream.getContext(), &driver,
-                                                     stream.getEventReceiver());
+            typename IOTraits<Stream>::Worker worker(*stream.getStreamContext(), &driver,
+                                                     stream.getStreamEventReceiver());
             LOG(DEBUG) << __func__ << ": starting worker...";
             ASSERT_TRUE(worker.start());
             LOG(DEBUG) << __func__ << ": joining worker...";
@@ -2950,63 +3247,59 @@
     if (ports.empty()) {
         GTEST_SKIP() << "No input mix ports for attached devices";
     }
+    bool atLeastOnePort = false;
     for (const auto& port : ports) {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(true, port);
-        ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for input mix port";
-        WithStream<IStreamIn> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
-        {
-            // The port of the stream is not connected, thus the list of active mics must be empty.
-            std::vector<MicrophoneDynamicInfo> activeMics;
-            EXPECT_IS_OK(stream.get()->getActiveMicrophones(&activeMics));
-            EXPECT_TRUE(activeMics.empty()) << "a stream on an unconnected port returns a "
-                                               "non-empty list of active microphones";
+        auto micDevicePorts = ModuleConfig::getBuiltInMicPorts(
+                moduleConfig->getConnectedSourceDevicesPortsForMixPort(port));
+        if (micDevicePorts.empty()) continue;
+        atLeastOnePort = true;
+        SCOPED_TRACE(port.toString());
+        StreamFixture<IStreamIn> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForPortsPair(module.get(), moduleConfig.get(),
+                                                               port, micDevicePorts[0]));
+        if (!stream.skipTestReason().empty()) continue;
+        std::vector<MicrophoneDynamicInfo> activeMics;
+        EXPECT_IS_OK(stream.getStream()->getActiveMicrophones(&activeMics));
+        EXPECT_FALSE(activeMics.empty());
+        for (const auto& mic : activeMics) {
+            EXPECT_NE(micInfos.end(),
+                      std::find_if(micInfos.begin(), micInfos.end(),
+                                   [&](const auto& micInfo) { return micInfo.id == mic.id; }))
+                    << "active microphone \"" << mic.id << "\" is not listed in "
+                    << "microphone infos returned by the module: "
+                    << ::android::internal::ToString(micInfos);
+            EXPECT_NE(0UL, mic.channelMapping.size())
+                    << "No channels specified for the microphone \"" << mic.id << "\"";
         }
-        if (auto micDevicePorts = ModuleConfig::getBuiltInMicPorts(
-                    moduleConfig->getConnectedSourceDevicesPortsForMixPort(port));
-            !micDevicePorts.empty()) {
-            auto devicePortConfig = moduleConfig->getSingleConfigForDevicePort(micDevicePorts[0]);
-            WithAudioPatch patch(true /*isInput*/, stream.getPortConfig(), devicePortConfig);
-            ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get()));
-            std::vector<MicrophoneDynamicInfo> activeMics;
-            EXPECT_IS_OK(stream.get()->getActiveMicrophones(&activeMics));
-            EXPECT_FALSE(activeMics.empty());
-            for (const auto& mic : activeMics) {
-                EXPECT_NE(micInfos.end(),
-                          std::find_if(micInfos.begin(), micInfos.end(),
-                                       [&](const auto& micInfo) { return micInfo.id == mic.id; }))
-                        << "active microphone \"" << mic.id << "\" is not listed in "
-                        << "microphone infos returned by the module: "
-                        << ::android::internal::ToString(micInfos);
-                EXPECT_NE(0UL, mic.channelMapping.size())
-                        << "No channels specified for the microphone \"" << mic.id << "\"";
-            }
-        }
-        {
-            // Now the port of the stream is not connected again, re-check that there are no
-            // active microphones.
-            std::vector<MicrophoneDynamicInfo> activeMics;
-            EXPECT_IS_OK(stream.get()->getActiveMicrophones(&activeMics));
-            EXPECT_TRUE(activeMics.empty()) << "a stream on an unconnected port returns a "
-                                               "non-empty list of active microphones";
-        }
+        stream.TeardownPatch();
+        // Now the port of the stream is not connected, check that there are no active microphones.
+        std::vector<MicrophoneDynamicInfo> emptyMics;
+        EXPECT_IS_OK(stream.getStream()->getActiveMicrophones(&emptyMics));
+        EXPECT_TRUE(emptyMics.empty()) << "a stream on an unconnected port returns a "
+                                          "non-empty list of active microphones";
+    }
+    if (!atLeastOnePort) {
+        GTEST_SKIP() << "No input mix ports could be routed to built-in microphone devices";
     }
 }
 
 TEST_P(AudioStreamIn, MicrophoneDirection) {
     using MD = IStreamIn::MicrophoneDirection;
-    const auto ports = moduleConfig->getInputMixPorts(true /*connectedOnly*/);
+    constexpr bool connectedOnly = true;
+    const auto ports = moduleConfig->getInputMixPorts(connectedOnly);
     if (ports.empty()) {
         GTEST_SKIP() << "No input mix ports for attached devices";
     }
-    bool isSupported = false;
+    bool isSupported = false, atLeastOnePort = false;
     for (const auto& port : ports) {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(true, port);
-        ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for input mix port";
-        WithStream<IStreamIn> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+        SCOPED_TRACE(port.toString());
+        StreamFixture<IStreamIn> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPort(module.get(), moduleConfig.get(), port,
+                                                             connectedOnly));
+        if (!stream.skipTestReason().empty()) continue;
+        atLeastOnePort = true;
         EXPECT_NO_FATAL_FAILURE(
-                TestAccessors<MD>(stream.get(), &IStreamIn::getMicrophoneDirection,
+                TestAccessors<MD>(stream.getStream(), &IStreamIn::getMicrophoneDirection,
                                   &IStreamIn::setMicrophoneDirection,
                                   std::vector<MD>(enum_range<MD>().begin(), enum_range<MD>().end()),
                                   {}, &isSupported));
@@ -3015,21 +3308,27 @@
     if (!isSupported) {
         GTEST_SKIP() << "Microphone direction is not supported";
     }
+    if (!atLeastOnePort) {
+        GTEST_SKIP() << "No input mix ports could be routed to built-in microphone devices";
+    }
 }
 
 TEST_P(AudioStreamIn, MicrophoneFieldDimension) {
-    const auto ports = moduleConfig->getInputMixPorts(true /*connectedOnly*/);
+    constexpr bool connectedOnly = true;
+    const auto ports = moduleConfig->getInputMixPorts(connectedOnly);
     if (ports.empty()) {
         GTEST_SKIP() << "No input mix ports for attached devices";
     }
-    bool isSupported = false;
+    bool isSupported = false, atLeastOnePort = false;
     for (const auto& port : ports) {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(true, port);
-        ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for input mix port";
-        WithStream<IStreamIn> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+        SCOPED_TRACE(port.toString());
+        StreamFixture<IStreamIn> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPort(module.get(), moduleConfig.get(), port,
+                                                             connectedOnly));
+        if (!stream.skipTestReason().empty()) continue;
+        atLeastOnePort = true;
         EXPECT_NO_FATAL_FAILURE(TestAccessors<float>(
-                stream.get(), &IStreamIn::getMicrophoneFieldDimension,
+                stream.getStream(), &IStreamIn::getMicrophoneFieldDimension,
                 &IStreamIn::setMicrophoneFieldDimension,
                 {IStreamIn::MIC_FIELD_DIMENSION_WIDE_ANGLE,
                  IStreamIn::MIC_FIELD_DIMENSION_WIDE_ANGLE / 2.0f,
@@ -3046,6 +3345,9 @@
     if (!isSupported) {
         GTEST_SKIP() << "Microphone direction is not supported";
     }
+    if (!atLeastOnePort) {
+        GTEST_SKIP() << "No input mix ports could be routed to built-in microphone devices";
+    }
 }
 
 TEST_P(AudioStreamOut, OpenTwicePrimary) {
@@ -3060,65 +3362,79 @@
 }
 
 TEST_P(AudioStreamOut, RequireOffloadInfo) {
+    constexpr bool connectedOnly = true;
     const auto offloadMixPorts =
-            moduleConfig->getOffloadMixPorts(true /*connectedOnly*/, true /*singlePort*/);
+            moduleConfig->getOffloadMixPorts(connectedOnly, true /*singlePort*/);
     if (offloadMixPorts.empty()) {
         GTEST_SKIP()
                 << "No mix port for compressed offload that could be routed to attached devices";
     }
-    const auto config = moduleConfig->getSingleConfigForMixPort(false, *offloadMixPorts.begin());
-    ASSERT_TRUE(config.has_value()) << "No profiles specified for the compressed offload mix port";
-    WithAudioPortConfig portConfig(config.value());
-    ASSERT_NO_FATAL_FAILURE(portConfig.SetUp(module.get()));
+    StreamFixture<IStreamOut> stream;
+    ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfigForMixPortOrConfig(
+            module.get(), moduleConfig.get(), *offloadMixPorts.begin(), connectedOnly));
+    if (auto reason = stream.skipTestReason(); !reason.empty()) {
+        GTEST_SKIP() << reason;
+    }
+    const auto portConfig = stream.getPortConfig();
     StreamDescriptor descriptor;
-    std::shared_ptr<IStreamOut> ignored;
     aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args;
-    args.portConfigId = portConfig.getId();
-    args.sourceMetadata = GenerateSourceMetadata(portConfig.get());
+    args.portConfigId = portConfig.id;
+    args.sourceMetadata = GenerateSourceMetadata(portConfig);
     args.bufferSizeFrames = kDefaultLargeBufferSizeFrames;
     aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret;
     EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, module->openOutputStream(args, &ret))
             << "when no offload info is provided for a compressed offload mix port";
+    if (ret.stream != nullptr) {
+        (void)WithStream<IStreamOut>::callClose(ret.stream);
+    }
 }
 
 TEST_P(AudioStreamOut, RequireAsyncCallback) {
+    constexpr bool connectedOnly = true;
     const auto nonBlockingMixPorts =
-            moduleConfig->getNonBlockingMixPorts(true /*connectedOnly*/, true /*singlePort*/);
+            moduleConfig->getNonBlockingMixPorts(connectedOnly, true /*singlePort*/);
     if (nonBlockingMixPorts.empty()) {
         GTEST_SKIP()
                 << "No mix port for non-blocking output that could be routed to attached devices";
     }
-    const auto config =
-            moduleConfig->getSingleConfigForMixPort(false, *nonBlockingMixPorts.begin());
-    ASSERT_TRUE(config.has_value()) << "No profiles specified for the non-blocking mix port";
-    WithAudioPortConfig portConfig(config.value());
-    ASSERT_NO_FATAL_FAILURE(portConfig.SetUp(module.get()));
+    StreamFixture<IStreamOut> stream;
+    ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfigForMixPortOrConfig(
+            module.get(), moduleConfig.get(), *nonBlockingMixPorts.begin(), connectedOnly));
+    if (auto reason = stream.skipTestReason(); !reason.empty()) {
+        GTEST_SKIP() << reason;
+    }
+    const auto portConfig = stream.getPortConfig();
     StreamDescriptor descriptor;
-    std::shared_ptr<IStreamOut> ignored;
     aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args;
-    args.portConfigId = portConfig.getId();
-    args.sourceMetadata = GenerateSourceMetadata(portConfig.get());
-    args.offloadInfo = ModuleConfig::generateOffloadInfoIfNeeded(portConfig.get());
-    args.bufferSizeFrames = kDefaultBufferSizeFrames;
+    args.portConfigId = portConfig.id;
+    args.sourceMetadata = GenerateSourceMetadata(portConfig);
+    args.offloadInfo = ModuleConfig::generateOffloadInfoIfNeeded(portConfig);
+    args.bufferSizeFrames = stream.getPatch().minimumStreamBufferSizeFrames;
     aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret;
     EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, module->openOutputStream(args, &ret))
             << "when no async callback is provided for a non-blocking mix port";
+    if (ret.stream != nullptr) {
+        (void)WithStream<IStreamOut>::callClose(ret.stream);
+    }
 }
 
 TEST_P(AudioStreamOut, AudioDescriptionMixLevel) {
-    const auto ports = moduleConfig->getOutputMixPorts(true /*connectedOnly*/);
+    constexpr bool connectedOnly = true;
+    const auto ports = moduleConfig->getOutputMixPorts(connectedOnly);
     if (ports.empty()) {
-        GTEST_SKIP() << "No output mix ports";
+        GTEST_SKIP() << "No output mix ports for attached devices";
     }
-    bool atLeastOneSupports = false;
+    bool atLeastOneSupports = false, atLeastOnePort = false;
     for (const auto& port : ports) {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(false, port);
-        ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for output mix port";
-        WithStream<IStreamOut> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+        SCOPED_TRACE(port.toString());
+        StreamFixture<IStreamOut> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPort(module.get(), moduleConfig.get(), port,
+                                                             connectedOnly));
+        if (!stream.skipTestReason().empty()) continue;
+        atLeastOnePort = true;
         bool isSupported = false;
         EXPECT_NO_FATAL_FAILURE(
-                TestAccessors<float>(stream.get(), &IStreamOut::getAudioDescriptionMixLevel,
+                TestAccessors<float>(stream.getStream(), &IStreamOut::getAudioDescriptionMixLevel,
                                      &IStreamOut::setAudioDescriptionMixLevel,
                                      {IStreamOut::AUDIO_DESCRIPTION_MIX_LEVEL_MAX,
                                       IStreamOut::AUDIO_DESCRIPTION_MIX_LEVEL_MAX - 1, 0,
@@ -3128,48 +3444,60 @@
                                      &isSupported));
         if (isSupported) atLeastOneSupports = true;
     }
+    if (!atLeastOnePort) {
+        GTEST_SKIP() << "No output mix ports could be routed to devices";
+    }
     if (!atLeastOneSupports) {
         GTEST_SKIP() << "Audio description mix level is not supported";
     }
 }
 
 TEST_P(AudioStreamOut, DualMonoMode) {
-    const auto ports = moduleConfig->getOutputMixPorts(true /*connectedOnly*/);
+    constexpr bool connectedOnly = true;
+    const auto ports = moduleConfig->getOutputMixPorts(connectedOnly);
     if (ports.empty()) {
-        GTEST_SKIP() << "No output mix ports";
+        GTEST_SKIP() << "No output mix ports for attached devices";
     }
-    bool atLeastOneSupports = false;
+    bool atLeastOneSupports = false, atLeastOnePort = false;
     for (const auto& port : ports) {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(false, port);
-        ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for output mix port";
-        WithStream<IStreamOut> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+        SCOPED_TRACE(port.toString());
+        StreamFixture<IStreamOut> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPort(module.get(), moduleConfig.get(), port,
+                                                             connectedOnly));
+        if (!stream.skipTestReason().empty()) continue;
+        atLeastOnePort = true;
         bool isSupported = false;
         EXPECT_NO_FATAL_FAILURE(TestAccessors<AudioDualMonoMode>(
-                stream.get(), &IStreamOut::getDualMonoMode, &IStreamOut::setDualMonoMode,
+                stream.getStream(), &IStreamOut::getDualMonoMode, &IStreamOut::setDualMonoMode,
                 std::vector<AudioDualMonoMode>(enum_range<AudioDualMonoMode>().begin(),
                                                enum_range<AudioDualMonoMode>().end()),
                 {}, &isSupported));
         if (isSupported) atLeastOneSupports = true;
     }
+    if (!atLeastOnePort) {
+        GTEST_SKIP() << "No output mix ports could be routed to devices";
+    }
     if (!atLeastOneSupports) {
         GTEST_SKIP() << "Audio dual mono mode is not supported";
     }
 }
 
 TEST_P(AudioStreamOut, LatencyMode) {
-    const auto ports = moduleConfig->getOutputMixPorts(true /*connectedOnly*/);
+    constexpr bool connectedOnly = true;
+    const auto ports = moduleConfig->getOutputMixPorts(connectedOnly);
     if (ports.empty()) {
-        GTEST_SKIP() << "No output mix ports";
+        GTEST_SKIP() << "No output mix ports for attached devices";
     }
-    bool atLeastOneSupports = false;
+    bool atLeastOneSupports = false, atLeastOnePort = false;
     for (const auto& port : ports) {
-        const auto portConfig = moduleConfig->getSingleConfigForMixPort(false, port);
-        ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for output mix port";
-        WithStream<IStreamOut> stream(portConfig.value());
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+        SCOPED_TRACE(port.toString());
+        StreamFixture<IStreamOut> stream;
+        ASSERT_NO_FATAL_FAILURE(stream.SetUpStreamForMixPort(module.get(), moduleConfig.get(), port,
+                                                             connectedOnly));
+        if (!stream.skipTestReason().empty()) continue;
+        atLeastOnePort = true;
         std::vector<AudioLatencyMode> supportedModes;
-        ndk::ScopedAStatus status = stream.get()->getRecommendedLatencyModes(&supportedModes);
+        ndk::ScopedAStatus status = stream.getStream()->getRecommendedLatencyModes(&supportedModes);
         if (status.getExceptionCode() == EX_UNSUPPORTED_OPERATION) continue;
         atLeastOneSupports = true;
         if (!status.isOk()) {
@@ -3181,7 +3509,7 @@
                                                     enum_range<AudioLatencyMode>().end());
         for (const auto mode : supportedModes) {
             unsupportedModes.erase(mode);
-            ndk::ScopedAStatus status = stream.get()->setLatencyMode(mode);
+            ndk::ScopedAStatus status = stream.getStream()->setLatencyMode(mode);
             if (status.getExceptionCode() == EX_UNSUPPORTED_OPERATION) {
                 ADD_FAILURE() << "When latency modes are supported, both getRecommendedLatencyModes"
                               << " and setLatencyMode must be supported";
@@ -3189,12 +3517,15 @@
             EXPECT_IS_OK(status) << "Setting of supported latency mode must succeed";
         }
         for (const auto mode : unsupportedModes) {
-            EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, stream.get()->setLatencyMode(mode));
+            EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, stream.getStream()->setLatencyMode(mode));
         }
     }
     if (!atLeastOneSupports) {
         GTEST_SKIP() << "Audio latency modes are not supported";
     }
+    if (!atLeastOnePort) {
+        GTEST_SKIP() << "No output mix ports could be routed to devices";
+    }
 }
 
 TEST_P(AudioStreamOut, PlaybackRate) {
@@ -3496,29 +3827,22 @@
         }
     }
 
-    bool ValidateObservablePosition(const AudioPortConfig& devicePortConfig) {
-        return !isTelephonyDeviceType(
-                devicePortConfig.ext.get<AudioPortExt::Tag::device>().device.type.type);
+    bool ValidateObservablePosition(const AudioDevice& device) {
+        return !isTelephonyDeviceType(device.type.type);
     }
 
     // Set up a patch first, then open a stream.
     void RunStreamIoCommandsImplSeq1(const AudioPortConfig& portConfig,
                                      std::shared_ptr<StateSequence> commandsAndStates,
                                      bool validatePositionIncrease) {
-        auto devicePorts = moduleConfig->getConnectedDevicesPortsForMixPort(
-                IOTraits<Stream>::is_input, portConfig);
-        ASSERT_FALSE(devicePorts.empty());
-        auto devicePortConfig = moduleConfig->getSingleConfigForDevicePort(devicePorts[0]);
-        SCOPED_TRACE(devicePortConfig.toString());
-        WithAudioPatch patch(IOTraits<Stream>::is_input, portConfig, devicePortConfig);
-        ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get()));
-
-        WithStream<Stream> stream(patch.getPortConfig(IOTraits<Stream>::is_input));
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+        StreamFixture<Stream> stream;
+        ASSERT_NO_FATAL_FAILURE(
+                stream.SetUpStreamForMixPortConfig(module.get(), moduleConfig.get(), portConfig));
+        ASSERT_EQ("", stream.skipTestReason());
         StreamLogicDefaultDriver driver(commandsAndStates,
-                                        stream.getContext()->getFrameSizeBytes());
-        typename IOTraits<Stream>::Worker worker(*stream.getContext(), &driver,
-                                                 stream.getEventReceiver());
+                                        stream.getStreamContext()->getFrameSizeBytes());
+        typename IOTraits<Stream>::Worker worker(*stream.getStreamContext(), &driver,
+                                                 stream.getStreamEventReceiver());
 
         LOG(DEBUG) << __func__ << ": starting worker...";
         ASSERT_TRUE(worker.start());
@@ -3526,7 +3850,7 @@
         worker.join();
         EXPECT_FALSE(worker.hasError()) << worker.getError();
         EXPECT_EQ("", driver.getUnexpectedStateTransition());
-        if (ValidateObservablePosition(devicePortConfig)) {
+        if (ValidateObservablePosition(stream.getDevice())) {
             if (validatePositionIncrease) {
                 EXPECT_TRUE(driver.hasObservablePositionIncrease());
             }
@@ -3534,24 +3858,21 @@
         }
     }
 
-    // Open a stream, then set up a patch for it.
+    // Open a stream, then set up a patch for it. Since first it is needed to get
+    // the minimum buffer size, a preliminary patch is set up, then removed.
     void RunStreamIoCommandsImplSeq2(const AudioPortConfig& portConfig,
                                      std::shared_ptr<StateSequence> commandsAndStates,
                                      bool validatePositionIncrease) {
-        WithStream<Stream> stream(portConfig);
-        ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
+        StreamFixture<Stream> stream;
+        ASSERT_NO_FATAL_FAILURE(
+                stream.SetUpPatchForMixPortConfig(module.get(), moduleConfig.get(), portConfig));
+        ASSERT_EQ("", stream.skipTestReason());
+        ASSERT_NO_FATAL_FAILURE(stream.TeardownPatchSetUpStream(module.get()));
         StreamLogicDefaultDriver driver(commandsAndStates,
-                                        stream.getContext()->getFrameSizeBytes());
-        typename IOTraits<Stream>::Worker worker(*stream.getContext(), &driver,
-                                                 stream.getEventReceiver());
-
-        auto devicePorts = moduleConfig->getConnectedDevicesPortsForMixPort(
-                IOTraits<Stream>::is_input, portConfig);
-        ASSERT_FALSE(devicePorts.empty());
-        auto devicePortConfig = moduleConfig->getSingleConfigForDevicePort(devicePorts[0]);
-        SCOPED_TRACE(devicePortConfig.toString());
-        WithAudioPatch patch(IOTraits<Stream>::is_input, stream.getPortConfig(), devicePortConfig);
-        ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get()));
+                                        stream.getStreamContext()->getFrameSizeBytes());
+        typename IOTraits<Stream>::Worker worker(*stream.getStreamContext(), &driver,
+                                                 stream.getStreamEventReceiver());
+        ASSERT_NO_FATAL_FAILURE(stream.ReconnectPatch(module.get()));
 
         LOG(DEBUG) << __func__ << ": starting worker...";
         ASSERT_TRUE(worker.start());
@@ -3559,7 +3880,7 @@
         worker.join();
         EXPECT_FALSE(worker.hasError()) << worker.getError();
         EXPECT_EQ("", driver.getUnexpectedStateTransition());
-        if (ValidateObservablePosition(devicePortConfig)) {
+        if (ValidateObservablePosition(stream.getDevice())) {
             if (validatePositionIncrease) {
                 EXPECT_TRUE(driver.hasObservablePositionIncrease());
             }
@@ -4253,191 +4574,154 @@
     explicit WithRemoteSubmix(AudioDeviceAddress address) : mAddress(address) {}
     WithRemoteSubmix(const WithRemoteSubmix&) = delete;
     WithRemoteSubmix& operator=(const WithRemoteSubmix&) = delete;
+
     static std::optional<AudioPort> getRemoteSubmixAudioPort(
             ModuleConfig* moduleConfig,
             const std::optional<AudioDeviceAddress>& address = std::nullopt) {
-        AudioDeviceType deviceType = IOTraits<Stream>::is_input ? AudioDeviceType::IN_SUBMIX
-                                                                : AudioDeviceType::OUT_SUBMIX;
-        auto ports = moduleConfig->getAudioPortsForDeviceTypes(
-                std::vector<AudioDeviceType>{deviceType},
-                AudioDeviceDescription::CONNECTION_VIRTUAL);
+        auto ports =
+                moduleConfig->getRemoteSubmixPorts(IOTraits<Stream>::is_input, true /*singlePort*/);
         if (ports.empty()) return {};
         AudioPort port = ports.front();
         if (address) {
             port.ext.template get<AudioPortExt::Tag::device>().device.address = address.value();
-        } else {
-            port = GenerateUniqueDeviceAddress(port);
         }
         return port;
     }
-    std::optional<AudioDeviceAddress> getAudioDeviceAddress() const { return mAddress; }
-    void SetUp(IModule* module, ModuleConfig* moduleConfig, const AudioPort& connectedPort) {
-        mModule = module;
-        mModuleConfig = moduleConfig;
 
-        ASSERT_NO_FATAL_FAILURE(SetupPatch(connectedPort));
-        if (!mSkipTest) {
-            // open stream
-            mStream = std::make_unique<WithStream<Stream>>(
-                    mPatch->getPortConfig(IOTraits<Stream>::is_input));
-            ASSERT_NO_FATAL_FAILURE(
-                    mStream->SetUp(mModule, AudioCoreModuleBase::kDefaultBufferSizeFrames));
-        }
-        mAddress = connectedPort.ext.template get<AudioPortExt::Tag::device>().device.address;
-    }
     void SetUp(IModule* module, ModuleConfig* moduleConfig) {
-        ASSERT_NO_FATAL_FAILURE(SetUpPortConnection(module, moduleConfig));
-        SetUp(module, moduleConfig, mConnectedPort->get());
+        auto devicePort = getRemoteSubmixAudioPort(moduleConfig, mAddress);
+        ASSERT_TRUE(devicePort.has_value()) << "Device port for remote submix device not found";
+        ASSERT_NO_FATAL_FAILURE(SetUp(module, moduleConfig, *devicePort));
     }
-    void sendBurstCommands() {
-        const StreamContext* context = mStream->getContext();
-        StreamLogicDefaultDriver driver(makeBurstCommands(true), context->getFrameSizeBytes());
-        typename IOTraits<Stream>::Worker worker(*context, &driver, mStream->getEventReceiver());
 
-        LOG(DEBUG) << __func__ << ": starting worker...";
-        ASSERT_TRUE(worker.start());
-        LOG(DEBUG) << __func__ << ": joining worker...";
-        worker.join();
-        EXPECT_FALSE(worker.hasError()) << worker.getError();
-        EXPECT_EQ("", driver.getUnexpectedStateTransition());
-        if (IOTraits<Stream>::is_input) {
-            EXPECT_TRUE(driver.hasObservablePositionIncrease());
-        }
-        EXPECT_FALSE(driver.hasRetrogradeObservablePosition());
+    void SendBurstCommandsStartWorker() {
+        const StreamContext* context = mStream->getStreamContext();
+        mWorkerDriver = std::make_unique<StreamLogicDefaultDriver>(makeBurstCommands(true),
+                                                                   context->getFrameSizeBytes());
+        mWorker = std::make_unique<typename IOTraits<Stream>::Worker>(
+                *context, mWorkerDriver.get(), mStream->getStreamEventReceiver());
+        LOG(DEBUG) << __func__ << ": starting " << IOTraits<Stream>::directionStr << " worker...";
+        ASSERT_TRUE(mWorker->start());
     }
-    bool skipTest() const { return mSkipTest; }
+
+    void SendBurstCommandsJoinWorker() {
+        // Must call 'prepareToClose' before attempting to join because the stream may be
+        // stuck due to absence of activity from the other side of the remote submix pipe.
+        std::shared_ptr<IStreamCommon> common;
+        ASSERT_IS_OK(mStream->getStream()->getStreamCommon(&common));
+        ASSERT_IS_OK(common->prepareToClose());
+        LOG(DEBUG) << __func__ << ": joining " << IOTraits<Stream>::directionStr << " worker...";
+        mWorker->join();
+        EXPECT_FALSE(mWorker->hasError()) << mWorker->getError();
+        EXPECT_EQ("", mWorkerDriver->getUnexpectedStateTransition());
+        if (IOTraits<Stream>::is_input) {
+            EXPECT_TRUE(mWorkerDriver->hasObservablePositionIncrease());
+        }
+        EXPECT_FALSE(mWorkerDriver->hasRetrogradeObservablePosition());
+        mWorker.reset();
+        mWorkerDriver.reset();
+    }
+
+    void SendBurstCommands() {
+        ASSERT_NO_FATAL_FAILURE(SendBurstCommandsStartWorker());
+        ASSERT_NO_FATAL_FAILURE(SendBurstCommandsJoinWorker());
+    }
+
+    std::optional<AudioDeviceAddress> getAudioDeviceAddress() const { return mAddress; }
+    std::string skipTestReason() const { return mStream->skipTestReason(); }
 
   private:
-    /* Connect remote submix external device */
-    void SetUpPortConnection(IModule* module, ModuleConfig* moduleConfig) {
-        auto port = getRemoteSubmixAudioPort(moduleConfig, mAddress);
-        ASSERT_TRUE(port.has_value()) << "Device AudioPort for remote submix not found";
-        mConnectedPort = std::make_unique<WithDevicePortConnectedState>(port.value());
-        ASSERT_NO_FATAL_FAILURE(mConnectedPort->SetUp(module, moduleConfig));
-    }
-    /* Get mix port config for stream and setup patch for it. */
-    void SetupPatch(const AudioPort& connectedPort) {
-        const auto portConfig =
-                mModuleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input);
-        if (!portConfig.has_value()) {
-            LOG(DEBUG) << __func__ << ": portConfig not found";
-            mSkipTest = true;
-            return;
-        }
-        auto devicePortConfig = mModuleConfig->getSingleConfigForDevicePort(connectedPort);
-        mPatch = std::make_unique<WithAudioPatch>(IOTraits<Stream>::is_input, portConfig.value(),
-                                                  devicePortConfig);
-        ASSERT_NO_FATAL_FAILURE(mPatch->SetUp(mModule));
+    void SetUp(IModule* module, ModuleConfig* moduleConfig, const AudioPort& devicePort) {
+        mStream = std::make_unique<StreamFixture<Stream>>();
+        ASSERT_NO_FATAL_FAILURE(
+                mStream->SetUpStreamForDevicePort(module, moduleConfig, devicePort));
+        mAddress = mStream->getDevice().address;
     }
 
-    bool mSkipTest = false;
-    IModule* mModule = nullptr;
-    ModuleConfig* mModuleConfig = nullptr;
     std::optional<AudioDeviceAddress> mAddress;
-    std::unique_ptr<WithDevicePortConnectedState> mConnectedPort;
-    std::unique_ptr<WithAudioPatch> mPatch;
-    std::unique_ptr<WithStream<Stream>> mStream;
+    std::unique_ptr<StreamFixture<Stream>> mStream;
+    std::unique_ptr<StreamLogicDefaultDriver> mWorkerDriver;
+    std::unique_ptr<typename IOTraits<Stream>::Worker> mWorker;
 };
 
 class AudioModuleRemoteSubmix : public AudioCoreModule {
   public:
     void SetUp() override {
-        ASSERT_NO_FATAL_FAILURE(AudioCoreModule::SetUp());
+        // Turn off "debug" which enables connections simulation. Since devices of the remote
+        // submix module are virtual, there is no need for simulation.
+        ASSERT_NO_FATAL_FAILURE(SetUpImpl(GetParam(), false /*setUpDebug*/));
         ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
     }
-
-    void TearDown() override { ASSERT_NO_FATAL_FAILURE(TearDownImpl()); }
 };
 
 TEST_P(AudioModuleRemoteSubmix, OutputDoesNotBlockWhenNoInput) {
-    // open output stream
     WithRemoteSubmix<IStreamOut> streamOut;
     ASSERT_NO_FATAL_FAILURE(streamOut.SetUp(module.get(), moduleConfig.get()));
-    if (streamOut.skipTest()) {
-        GTEST_SKIP() << "No mix port for attached devices";
-    }
-    // write something to stream
-    ASSERT_NO_FATAL_FAILURE(streamOut.sendBurstCommands());
+    // Note: here and in other tests any issue with connection attempts is considered as a problem.
+    ASSERT_EQ("", streamOut.skipTestReason());
+    ASSERT_NO_FATAL_FAILURE(streamOut.SendBurstCommands());
 }
 
 TEST_P(AudioModuleRemoteSubmix, OutputDoesNotBlockWhenInputStuck) {
-    // open output stream
     WithRemoteSubmix<IStreamOut> streamOut;
     ASSERT_NO_FATAL_FAILURE(streamOut.SetUp(module.get(), moduleConfig.get()));
-    if (streamOut.skipTest()) {
-        GTEST_SKIP() << "No mix port for attached devices";
-    }
+    ASSERT_EQ("", streamOut.skipTestReason());
     auto address = streamOut.getAudioDeviceAddress();
     ASSERT_TRUE(address.has_value());
 
-    // open input stream
     WithRemoteSubmix<IStreamIn> streamIn(address.value());
     ASSERT_NO_FATAL_FAILURE(streamIn.SetUp(module.get(), moduleConfig.get()));
-    if (streamIn.skipTest()) {
-        GTEST_SKIP() << "No mix port for attached devices";
-    }
+    ASSERT_EQ("", streamIn.skipTestReason());
 
-    // write something to stream
-    ASSERT_NO_FATAL_FAILURE(streamOut.sendBurstCommands());
+    ASSERT_NO_FATAL_FAILURE(streamOut.SendBurstCommands());
 }
 
 TEST_P(AudioModuleRemoteSubmix, OutputAndInput) {
-    // open output stream
     WithRemoteSubmix<IStreamOut> streamOut;
     ASSERT_NO_FATAL_FAILURE(streamOut.SetUp(module.get(), moduleConfig.get()));
-    if (streamOut.skipTest()) {
-        GTEST_SKIP() << "No mix port for attached devices";
-    }
+    ASSERT_EQ("", streamOut.skipTestReason());
     auto address = streamOut.getAudioDeviceAddress();
     ASSERT_TRUE(address.has_value());
 
-    // open input stream
     WithRemoteSubmix<IStreamIn> streamIn(address.value());
     ASSERT_NO_FATAL_FAILURE(streamIn.SetUp(module.get(), moduleConfig.get()));
-    if (streamIn.skipTest()) {
-        GTEST_SKIP() << "No mix port for attached devices";
-    }
+    ASSERT_EQ("", streamIn.skipTestReason());
 
-    // write something to stream
-    ASSERT_NO_FATAL_FAILURE(streamOut.sendBurstCommands());
-    // read from input stream
-    ASSERT_NO_FATAL_FAILURE(streamIn.sendBurstCommands());
+    // Start writing into the output stream.
+    ASSERT_NO_FATAL_FAILURE(streamOut.SendBurstCommandsStartWorker());
+    // Simultaneously, read from the input stream.
+    ASSERT_NO_FATAL_FAILURE(streamIn.SendBurstCommands());
+    ASSERT_NO_FATAL_FAILURE(streamOut.SendBurstCommandsJoinWorker());
 }
 
 TEST_P(AudioModuleRemoteSubmix, OpenInputMultipleTimes) {
-    // open output stream
     WithRemoteSubmix<IStreamOut> streamOut;
     ASSERT_NO_FATAL_FAILURE(streamOut.SetUp(module.get(), moduleConfig.get()));
-    if (streamOut.skipTest()) {
-        GTEST_SKIP() << "No mix port for attached devices";
-    }
+    ASSERT_EQ("", streamOut.skipTestReason());
     auto address = streamOut.getAudioDeviceAddress();
     ASSERT_TRUE(address.has_value());
 
-    // connect remote submix input device port
-    auto port = WithRemoteSubmix<IStreamIn>::getRemoteSubmixAudioPort(moduleConfig.get(),
-                                                                      address.value());
-    ASSERT_TRUE(port.has_value()) << "Device AudioPort for remote submix not found";
-    WithDevicePortConnectedState connectedInputPort(port.value());
-    ASSERT_NO_FATAL_FAILURE(connectedInputPort.SetUp(module.get(), moduleConfig.get()));
-
-    // open input streams
-    const int streamInCount = 3;
+    const size_t streamInCount = 3;
     std::vector<std::unique_ptr<WithRemoteSubmix<IStreamIn>>> streamIns(streamInCount);
-    for (int i = 0; i < streamInCount; i++) {
-        streamIns[i] = std::make_unique<WithRemoteSubmix<IStreamIn>>();
-        ASSERT_NO_FATAL_FAILURE(
-                streamIns[i]->SetUp(module.get(), moduleConfig.get(), connectedInputPort.get()));
-        if (streamIns[i]->skipTest()) {
-            GTEST_SKIP() << "No mix port for attached devices";
-        }
+    for (size_t i = 0; i < streamInCount; i++) {
+        streamIns[i] = std::make_unique<WithRemoteSubmix<IStreamIn>>(address.value());
+        ASSERT_NO_FATAL_FAILURE(streamIns[i]->SetUp(module.get(), moduleConfig.get()));
+        ASSERT_EQ("", streamIns[i]->skipTestReason());
     }
-    // write something to output stream
-    ASSERT_NO_FATAL_FAILURE(streamOut.sendBurstCommands());
-
-    // read from input streams
-    for (int i = 0; i < streamInCount; i++) {
-        ASSERT_NO_FATAL_FAILURE(streamIns[i]->sendBurstCommands());
+    // Start writing into the output stream.
+    ASSERT_NO_FATAL_FAILURE(streamOut.SendBurstCommandsStartWorker());
+    // Simultaneously, read from input streams.
+    for (size_t i = 0; i < streamInCount; i++) {
+        ASSERT_NO_FATAL_FAILURE(streamIns[i]->SendBurstCommandsStartWorker());
+    }
+    for (size_t i = 0; i < streamInCount; i++) {
+        ASSERT_NO_FATAL_FAILURE(streamIns[i]->SendBurstCommandsJoinWorker());
+    }
+    ASSERT_NO_FATAL_FAILURE(streamOut.SendBurstCommandsJoinWorker());
+    // Clean up input streams in the reverse order because the device connection is owned
+    // by the first one.
+    for (size_t i = streamInCount; i != 0; --i) {
+        streamIns[i - 1].reset();
     }
 }
 
diff --git a/audio/common/all-versions/default/service/android.hardware.audio.service.rc b/audio/common/all-versions/default/service/android.hardware.audio.service.rc
index 0de4eea..a1df67a 100644
--- a/audio/common/all-versions/default/service/android.hardware.audio.service.rc
+++ b/audio/common/all-versions/default/service/android.hardware.audio.service.rc
@@ -2,7 +2,7 @@
     class hal
     user audioserver
     # media gid needed for /dev/fm (radio) and for /data/misc/media (tee)
-    group audio camera drmrpc inet media mediadrm net_bt net_bt_admin net_bw_acct wakelock context_hub
+    group audio camera drmrpc inet media mediadrm net_bt net_bt_admin net_bw_acct wakelock context_hub system
     capabilities BLOCK_SUSPEND SYS_NICE
     # setting RLIMIT_RTPRIO allows binder RT priority inheritance
     rlimit rtprio 10 10
diff --git a/automotive/evs/aidl/impl/default/Android.bp b/automotive/evs/aidl/impl/default/Android.bp
index 6b638a3..3d5b7c4 100644
--- a/automotive/evs/aidl/impl/default/Android.bp
+++ b/automotive/evs/aidl/impl/default/Android.bp
@@ -24,6 +24,9 @@
 cc_defaults {
     name: "android.hardware.automotive.evs-aidl-default-service-default",
     defaults: ["EvsHalDefaults"],
+    header_libs: [
+        "libstagefright_headers",
+    ],
     shared_libs: [
         "android.hardware.graphics.bufferqueue@1.0",
         "android.hardware.graphics.bufferqueue@2.0",
@@ -35,6 +38,7 @@
         "libcamera_metadata",
         "libhardware_legacy",
         "libhidlbase",
+        "libmediandk",
         "libnativewindow",
         "libtinyxml2",
         "libui",
diff --git a/automotive/evs/aidl/impl/default/include/EvsVideoEmulatedCamera.h b/automotive/evs/aidl/impl/default/include/EvsVideoEmulatedCamera.h
index 356a42a..a850d65 100644
--- a/automotive/evs/aidl/impl/default/include/EvsVideoEmulatedCamera.h
+++ b/automotive/evs/aidl/impl/default/include/EvsVideoEmulatedCamera.h
@@ -26,20 +26,27 @@
 #include <aidl/android/hardware/automotive/evs/IEvsDisplay.h>
 #include <aidl/android/hardware/automotive/evs/ParameterRange.h>
 #include <aidl/android/hardware/automotive/evs/Stream.h>
+#include <media/NdkMediaExtractor.h>
+
+#include <ui/GraphicBuffer.h>
 
 #include <cstdint>
 #include <memory>
+#include <thread>
 #include <unordered_map>
 #include <vector>
 
 namespace aidl::android::hardware::automotive::evs::implementation {
 
 class EvsVideoEmulatedCamera : public EvsCamera {
+  private:
+    using Base = EvsCamera;
+
   public:
     EvsVideoEmulatedCamera(Sigil sigil, const char* deviceName,
                            std::unique_ptr<ConfigManager::CameraInfo>& camInfo);
 
-    ~EvsVideoEmulatedCamera() override;
+    ~EvsVideoEmulatedCamera() override = default;
 
     // Methods from ::android::hardware::automotive::evs::IEvsCamera follow.
     ndk::ScopedAStatus forcePrimaryClient(
@@ -60,6 +67,9 @@
     ndk::ScopedAStatus setPrimaryClient() override;
     ndk::ScopedAStatus unsetPrimaryClient() override;
 
+    // Methods from EvsCameraBase follow.
+    void shutdown() override;
+
     const evs::CameraDesc& getDesc() { return mDescription; }
 
     static std::shared_ptr<EvsVideoEmulatedCamera> Create(const char* deviceName);
@@ -81,8 +91,18 @@
         int32_t value;
     };
 
+    bool initialize();
+
+    void generateFrames();
+
+    void renderOneFrame();
+
     void initializeParameters();
 
+    void onCodecInputAvailable(const int32_t index);
+
+    void onCodecOutputAvailable(const int32_t index, const AMediaCodecBufferInfo& info);
+
     ::android::status_t allocateOneFrame(buffer_handle_t* handle) override;
 
     bool startVideoStreamImpl_locked(const std::shared_ptr<evs::IEvsCameraStream>& receiver,
@@ -92,9 +112,42 @@
     bool stopVideoStreamImpl_locked(ndk::ScopedAStatus& status,
                                     std::unique_lock<std::mutex>& lck) override;
 
+    bool postVideoStreamStop_locked(ndk::ScopedAStatus& status,
+                                    std::unique_lock<std::mutex>& lck) override;
+
     // The properties of this camera.
     CameraDesc mDescription = {};
 
+    std::thread mCaptureThread;
+
+    // The callback used to deliver each frame
+    std::shared_ptr<evs::IEvsCameraStream> mStream;
+
+    std::string mVideoFileName;
+    // Media decoder resources - Owned by mDecoderThead when thread is running.
+    int mVideoFd = 0;
+
+    struct AMediaExtractorDeleter {
+        void operator()(AMediaExtractor* extractor) const { AMediaExtractor_delete(extractor); }
+    };
+    struct AMediaCodecDeleter {
+        void operator()(AMediaCodec* codec) const { AMediaCodec_delete(codec); }
+    };
+
+    std::unique_ptr<AMediaExtractor, AMediaExtractorDeleter> mVideoExtractor;
+    std::unique_ptr<AMediaCodec, AMediaCodecDeleter> mVideoCodec;
+
+    // Horizontal pixel count in the buffers
+    int32_t mWidth = 0;
+    // Vertical pixel count in the buffers
+    int32_t mHeight = 0;
+    // Values from android_pixel_format_t
+    uint32_t mFormat = 0;
+    // Values from from Gralloc.h
+    uint64_t mUsage = 0;
+    // Bytes per line in the buffers
+    uint32_t mStride = 0;
+
     // Camera parameters.
     std::unordered_map<CameraParam, std::shared_ptr<CameraParameterDesc>> mParams;
 
diff --git a/automotive/evs/aidl/impl/default/src/EvsVideoEmulatedCamera.cpp b/automotive/evs/aidl/impl/default/src/EvsVideoEmulatedCamera.cpp
index f198a64..8181e47 100644
--- a/automotive/evs/aidl/impl/default/src/EvsVideoEmulatedCamera.cpp
+++ b/automotive/evs/aidl/impl/default/src/EvsVideoEmulatedCamera.cpp
@@ -18,17 +18,35 @@
 
 #include <aidl/android/hardware/automotive/evs/EvsResult.h>
 
+#include <aidlcommonsupport/NativeHandle.h>
 #include <android-base/logging.h>
+#include <android-base/strings.h>
+#include <media/stagefright/MediaCodecConstants.h>
+#include <ui/GraphicBufferAllocator.h>
+#include <utils/SystemClock.h>
 
+#include <fcntl.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <chrono>
 #include <cstddef>
 #include <cstdint>
+#include <tuple>
+#include <utility>
 
 namespace aidl::android::hardware::automotive::evs::implementation {
 
+namespace {
+struct FormatDeleter {
+    void operator()(AMediaFormat* format) const { AMediaFormat_delete(format); }
+};
+}  // namespace
+
 EvsVideoEmulatedCamera::EvsVideoEmulatedCamera(Sigil, const char* deviceName,
                                                std::unique_ptr<ConfigManager::CameraInfo>& camInfo)
-    : mCameraInfo(camInfo) {
-    mDescription.id = deviceName;
+    : mVideoFileName(deviceName), mCameraInfo(camInfo) {
+    mDescription.id = mVideoFileName;
 
     /* set camera metadata */
     if (camInfo) {
@@ -40,6 +58,256 @@
     initializeParameters();
 }
 
+bool EvsVideoEmulatedCamera::initialize() {
+    // Open file.
+    mVideoFd = open(mVideoFileName.c_str(), 0, O_RDONLY);
+    if (mVideoFd < 0) {
+        PLOG(ERROR) << __func__ << ": Failed to open video file \"" << mVideoFileName << "\".";
+        return false;
+    }
+
+    // Initialize Media Extractor.
+    {
+        mVideoExtractor.reset(AMediaExtractor_new());
+        off64_t filesize = lseek64(mVideoFd, 0, SEEK_END);
+        lseek(mVideoFd, 0, SEEK_SET);
+        const media_status_t status =
+                AMediaExtractor_setDataSourceFd(mVideoExtractor.get(), mVideoFd, 0, filesize);
+        if (status != AMEDIA_OK) {
+            LOG(ERROR) << __func__
+                       << ": Received error when initializing media extractor. Error code: "
+                       << status << ".";
+            return false;
+        }
+    }
+
+    // Initialize Media Codec and file format.
+    std::unique_ptr<AMediaFormat, FormatDeleter> format;
+    const char* mime;
+    bool selected = false;
+    int numTracks = AMediaExtractor_getTrackCount(mVideoExtractor.get());
+    for (int i = 0; i < numTracks; i++) {
+        format.reset(AMediaExtractor_getTrackFormat(mVideoExtractor.get(), i));
+        if (!AMediaFormat_getString(format.get(), AMEDIAFORMAT_KEY_MIME, &mime)) {
+            LOG(ERROR) << __func__ << ": Error in fetching format string";
+            continue;
+        }
+        if (!::android::base::StartsWith(mime, "video/")) {
+            continue;
+        }
+        const media_status_t status = AMediaExtractor_selectTrack(mVideoExtractor.get(), i);
+        if (status != AMEDIA_OK) {
+            LOG(ERROR) << __func__
+                       << ": Media extractor returned error to select track. Error Code: " << status
+                       << ".";
+            return false;
+        }
+        selected = true;
+        break;
+    }
+    if (!selected) {
+        LOG(ERROR) << __func__ << ": No video track in video file \"" << mVideoFileName << "\".";
+        return false;
+    }
+
+    mVideoCodec.reset(AMediaCodec_createDecoderByType(mime));
+    if (!mVideoCodec) {
+        LOG(ERROR) << __func__ << ": Unable to create decoder.";
+        return false;
+    }
+
+    mDescription.vendorFlags = 0xFFFFFFFF;  // Arbitrary test value
+    mUsage = GRALLOC_USAGE_HW_TEXTURE | GRALLOC_USAGE_HW_CAMERA_WRITE |
+             GRALLOC_USAGE_SW_READ_RARELY | GRALLOC_USAGE_SW_WRITE_RARELY;
+    mFormat = HAL_PIXEL_FORMAT_YCBCR_420_888;
+    AMediaFormat_setInt32(format.get(), AMEDIAFORMAT_KEY_COLOR_FORMAT, COLOR_FormatYUV420Flexible);
+    {
+        const media_status_t status =
+                AMediaCodec_configure(mVideoCodec.get(), format.get(), nullptr, nullptr, 0);
+        if (status != AMEDIA_OK) {
+            LOG(ERROR) << __func__
+                       << ": Received error in configuring mCodec. Error code: " << status << ".";
+            return false;
+        }
+    }
+    format.reset(AMediaCodec_getOutputFormat(mVideoCodec.get()));
+    AMediaFormat_getInt32(format.get(), AMEDIAFORMAT_KEY_WIDTH, &mWidth);
+    AMediaFormat_getInt32(format.get(), AMEDIAFORMAT_KEY_HEIGHT, &mHeight);
+    return true;
+}
+
+void EvsVideoEmulatedCamera::generateFrames() {
+    while (true) {
+        {
+            std::lock_guard lock(mMutex);
+            if (mStreamState != StreamState::RUNNING) {
+                return;
+            }
+        }
+        renderOneFrame();
+    }
+}
+
+void EvsVideoEmulatedCamera::onCodecInputAvailable(const int32_t index) {
+    const size_t sampleSize = AMediaExtractor_getSampleSize(mVideoExtractor.get());
+    const int64_t presentationTime = AMediaExtractor_getSampleTime(mVideoExtractor.get());
+    size_t bufferSize = 0;
+    uint8_t* const codecInputBuffer =
+            AMediaCodec_getInputBuffer(mVideoCodec.get(), index, &bufferSize);
+    if (sampleSize > bufferSize) {
+        LOG(ERROR) << __func__ << ": Buffer is not large enough.";
+    }
+    if (presentationTime < 0) {
+        AMediaCodec_queueInputBuffer(mVideoCodec.get(), index, /* offset = */ 0,
+                                     /* size = */ 0, presentationTime,
+                                     AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM);
+        LOG(INFO) << __func__ << ": Reaching the end of stream.";
+        return;
+    }
+    const size_t readSize =
+            AMediaExtractor_readSampleData(mVideoExtractor.get(), codecInputBuffer, sampleSize);
+    const media_status_t status = AMediaCodec_queueInputBuffer(
+            mVideoCodec.get(), index, /*offset = */ 0, readSize, presentationTime, /* flags = */ 0);
+    if (status != AMEDIA_OK) {
+        LOG(ERROR) << __func__
+                   << ": Received error in queueing input buffer. Error code: " << status;
+    }
+}
+
+void EvsVideoEmulatedCamera::onCodecOutputAvailable(const int32_t index,
+                                                    const AMediaCodecBufferInfo& info) {
+    using std::chrono::duration_cast;
+    using std::chrono::microseconds;
+    using std::chrono::nanoseconds;
+    using AidlPixelFormat = ::aidl::android::hardware::graphics::common::PixelFormat;
+    using ::aidl::android::hardware::graphics::common::BufferUsage;
+
+    size_t decodedOutSize = 0;
+    uint8_t* const codecOutputBuffer =
+            AMediaCodec_getOutputBuffer(mVideoCodec.get(), index, &decodedOutSize) + info.offset;
+
+    std::size_t renderBufferId = static_cast<std::size_t>(-1);
+    buffer_handle_t renderBufferHandle = nullptr;
+    {
+        std::lock_guard lock(mMutex);
+        if (mStreamState != StreamState::RUNNING) {
+            return;
+        }
+        std::tie(renderBufferId, renderBufferHandle) = useBuffer_unsafe();
+    }
+    if (!renderBufferHandle) {
+        LOG(ERROR) << __func__ << ": Camera failed to get an available render buffer.";
+        return;
+    }
+    std::vector<BufferDesc> renderBufferDescs;
+    renderBufferDescs.push_back({
+            .buffer =
+                    {
+                            .description =
+                                    {
+                                            .width = static_cast<int32_t>(mWidth),
+                                            .height = static_cast<int32_t>(mHeight),
+                                            .layers = 1,
+                                            .format = static_cast<AidlPixelFormat>(mFormat),
+                                            .usage = static_cast<BufferUsage>(mUsage),
+                                            .stride = static_cast<int32_t>(mStride),
+                                    },
+                            .handle = ::android::dupToAidl(renderBufferHandle),
+                    },
+            .bufferId = static_cast<int32_t>(renderBufferId),
+            .deviceId = mDescription.id,
+            .timestamp = duration_cast<microseconds>(nanoseconds(::android::elapsedRealtimeNano()))
+                                 .count(),
+    });
+
+    // Lock our output buffer for writing
+    uint8_t* pixels = nullptr;
+    int32_t bytesPerStride = 0;
+    auto& mapper = ::android::GraphicBufferMapper::get();
+    mapper.lock(renderBufferHandle, GRALLOC_USAGE_SW_WRITE_OFTEN | GRALLOC_USAGE_SW_READ_NEVER,
+                ::android::Rect(mWidth, mHeight), (void**)&pixels, nullptr, &bytesPerStride);
+
+    // If we failed to lock the pixel buffer, we're about to crash, but log it first
+    if (!pixels) {
+        LOG(ERROR) << __func__ << ": Camera failed to gain access to image buffer for writing";
+        return;
+    }
+
+    std::size_t ySize = mHeight * mStride;
+    std::size_t uvSize = ySize / 4;
+
+    std::memcpy(pixels, codecOutputBuffer, ySize);
+    pixels += ySize;
+
+    uint8_t* u_head = codecOutputBuffer + ySize;
+    uint8_t* v_head = u_head + uvSize;
+
+    for (size_t i = 0; i < uvSize; ++i) {
+        *(pixels++) = *(u_head++);
+        *(pixels++) = *(v_head++);
+    }
+
+    const auto status =
+            AMediaCodec_releaseOutputBuffer(mVideoCodec.get(), index, /* render = */ false);
+    if (status != AMEDIA_OK) {
+        LOG(ERROR) << __func__
+                   << ": Received error in releasing output buffer. Error code: " << status;
+    }
+
+    // Release our output buffer
+    mapper.unlock(renderBufferHandle);
+
+    // Issue the (asynchronous) callback to the client -- can't be holding the lock
+    if (mStream && mStream->deliverFrame(renderBufferDescs).isOk()) {
+        LOG(DEBUG) << __func__ << ": Delivered " << renderBufferHandle
+                   << ", id = " << renderBufferId;
+    } else {
+        // This can happen if the client dies and is likely unrecoverable.
+        // To avoid consuming resources generating failing calls, we stop sending
+        // frames.  Note, however, that the stream remains in the "STREAMING" state
+        // until cleaned up on the main thread.
+        LOG(ERROR) << __func__ << ": Frame delivery call failed in the transport layer.";
+        doneWithFrame(renderBufferDescs);
+    }
+}
+
+void EvsVideoEmulatedCamera::renderOneFrame() {
+    using std::chrono::duration_cast;
+    using std::chrono::microseconds;
+    using namespace std::chrono_literals;
+
+    // push to codec input
+    while (true) {
+        int codecInputBufferIdx =
+                AMediaCodec_dequeueInputBuffer(mVideoCodec.get(), /* timeoutUs = */ 0);
+        if (codecInputBufferIdx < 0) {
+            if (codecInputBufferIdx != AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
+                LOG(ERROR) << __func__
+                           << ": Received error in AMediaCodec_dequeueInputBuffer. Error code: "
+                           << codecInputBufferIdx;
+            }
+            break;
+        }
+        onCodecInputAvailable(codecInputBufferIdx);
+        AMediaExtractor_advance(mVideoExtractor.get());
+    }
+
+    // pop from codec output
+
+    AMediaCodecBufferInfo info;
+    int codecOutputputBufferIdx = AMediaCodec_dequeueOutputBuffer(
+            mVideoCodec.get(), &info, /* timeoutUs = */ duration_cast<microseconds>(1ms).count());
+    if (codecOutputputBufferIdx < 0) {
+        if (codecOutputputBufferIdx != AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
+            LOG(ERROR) << __func__
+                       << ": Received error in AMediaCodec_dequeueOutputBuffer. Error code: "
+                       << codecOutputputBufferIdx;
+        }
+        return;
+    }
+    onCodecOutputAvailable(codecOutputputBufferIdx, info);
+}
+
 void EvsVideoEmulatedCamera::initializeParameters() {
     mParams.emplace(
             CameraParam::BRIGHTNESS,
@@ -52,22 +320,54 @@
             new CameraParameterDesc(/* min= */ 0, /* max= */ 255, /* step= */ 1, /* value= */ 255));
 }
 
-::android::status_t EvsVideoEmulatedCamera::allocateOneFrame(buffer_handle_t* /* handle */) {
-    LOG(FATAL) << __func__ << ": Not implemented yet.";
-    return ::android::UNKNOWN_ERROR;
+::android::status_t EvsVideoEmulatedCamera::allocateOneFrame(buffer_handle_t* handle) {
+    static auto& alloc = ::android::GraphicBufferAllocator::get();
+    unsigned pixelsPerLine = 0;
+    const auto result = alloc.allocate(mWidth, mHeight, mFormat, 1, mUsage, handle, &pixelsPerLine,
+                                       0, "EvsVideoEmulatedCamera");
+    if (mStride == 0) {
+        // Gralloc defines stride in terms of pixels per line
+        mStride = pixelsPerLine;
+    } else if (mStride != pixelsPerLine) {
+        LOG(ERROR) << "We did not expect to get buffers with different strides!";
+    }
+    return result;
 }
 
 bool EvsVideoEmulatedCamera::startVideoStreamImpl_locked(
-        const std::shared_ptr<evs::IEvsCameraStream>& /* receiver */,
-        ndk::ScopedAStatus& /* status */, std::unique_lock<std::mutex>& /* lck */) {
-    LOG(FATAL) << __func__ << ": Not implemented yet.";
-    return false;
+        const std::shared_ptr<evs::IEvsCameraStream>& receiver, ndk::ScopedAStatus& /* status */,
+        std::unique_lock<std::mutex>& /* lck */) {
+    mStream = receiver;
+
+    const media_status_t status = AMediaCodec_start(mVideoCodec.get());
+    if (status != AMEDIA_OK) {
+        LOG(ERROR) << __func__ << ": Received error in starting decoder. Error code: " << status
+                   << ".";
+        return false;
+    }
+    mCaptureThread = std::thread([this]() { generateFrames(); });
+
+    return true;
 }
 
 bool EvsVideoEmulatedCamera::stopVideoStreamImpl_locked(ndk::ScopedAStatus& /* status */,
-                                                        std::unique_lock<std::mutex>& /* lck */) {
-    LOG(FATAL) << __func__ << ": Not implemented yet.";
-    return false;
+                                                        std::unique_lock<std::mutex>& lck) {
+    const media_status_t status = AMediaCodec_stop(mVideoCodec.get());
+    lck.unlock();
+    if (mCaptureThread.joinable()) {
+        mCaptureThread.join();
+    }
+    lck.lock();
+    return status == AMEDIA_OK;
+}
+
+bool EvsVideoEmulatedCamera::postVideoStreamStop_locked(ndk::ScopedAStatus& status,
+                                                        std::unique_lock<std::mutex>& lck) {
+    if (!Base::postVideoStreamStop_locked(status, lck)) {
+        return false;
+    }
+    mStream = nullptr;
+    return true;
 }
 
 ndk::ScopedAStatus EvsVideoEmulatedCamera::forcePrimaryClient(
@@ -189,10 +489,19 @@
         LOG(ERROR) << "Failed to instantiate EvsVideoEmulatedCamera.";
         return nullptr;
     }
-    c->mDescription.vendorFlags = 0xFFFFFFFF;  // Arbitrary test value
+    if (!c->initialize()) {
+        LOG(ERROR) << "Failed to initialize EvsVideoEmulatedCamera.";
+        return nullptr;
+    }
     return c;
 }
 
-EvsVideoEmulatedCamera::~EvsVideoEmulatedCamera() {}
+void EvsVideoEmulatedCamera::shutdown() {
+    mVideoCodec.reset();
+    mVideoExtractor.reset();
+    close(mVideoFd);
+    mVideoFd = 0;
+    Base::shutdown();
+}
 
 }  // namespace aidl::android::hardware::automotive::evs::implementation
diff --git a/cas/aidl/default/service.cpp b/cas/aidl/default/service.cpp
index bed2f01..076c7bb 100644
--- a/cas/aidl/default/service.cpp
+++ b/cas/aidl/default/service.cpp
@@ -31,6 +31,7 @@
 
 int main() {
     ABinderProcess_setThreadPoolMaxThreadCount(8);
+    ABinderProcess_startThreadPool();
 
     // Setup hwbinder service
     std::shared_ptr<MediaCasService> service = ::ndk::SharedRefBase::make<MediaCasService>();
diff --git a/compatibility_matrices/compatibility_matrix.9.xml b/compatibility_matrices/compatibility_matrix.9.xml
index 2c178cd..1ea8b62 100644
--- a/compatibility_matrices/compatibility_matrix.9.xml
+++ b/compatibility_matrices/compatibility_matrix.9.xml
@@ -609,7 +609,7 @@
     </hal>
     <hal format="aidl" optional="true">
         <name>android.hardware.usb</name>
-        <version>1-2</version>
+        <version>1-3</version>
         <interface>
             <name>IUsb</name>
             <instance>default</instance>
diff --git a/compatibility_matrices/exclude/fcm_exclude.cpp b/compatibility_matrices/exclude/fcm_exclude.cpp
index 2cb4ffa..46f0e03 100644
--- a/compatibility_matrices/exclude/fcm_exclude.cpp
+++ b/compatibility_matrices/exclude/fcm_exclude.cpp
@@ -128,6 +128,7 @@
             "android.hardware.media.bufferpool2@",
             "android.hardware.radio@",
             "android.hardware.uwb.fira_android@",
+            "android.hardware.wifi.common@",
 
             // Test packages are exempted.
             "android.hardware.tests.",
diff --git a/graphics/composer/aidl/include/android/hardware/graphics/composer3/ComposerClientWriter.h b/graphics/composer/aidl/include/android/hardware/graphics/composer3/ComposerClientWriter.h
index 2e902e5..92ed6d3 100644
--- a/graphics/composer/aidl/include/android/hardware/graphics/composer3/ComposerClientWriter.h
+++ b/graphics/composer/aidl/include/android/hardware/graphics/composer3/ComposerClientWriter.h
@@ -98,17 +98,21 @@
     }
 
     void validateDisplay(int64_t display,
-                         std::optional<ClockMonotonicTimestamp> expectedPresentTime) {
+                         std::optional<ClockMonotonicTimestamp> expectedPresentTime,
+                         int32_t frameIntervalNs) {
         auto& command = getDisplayCommand(display);
         command.expectedPresentTime = expectedPresentTime;
         command.validateDisplay = true;
+        command.frameIntervalNs = frameIntervalNs;
     }
 
     void presentOrvalidateDisplay(int64_t display,
-                                  std::optional<ClockMonotonicTimestamp> expectedPresentTime) {
+                                  std::optional<ClockMonotonicTimestamp> expectedPresentTime,
+                                  int32_t frameIntervalNs) {
         auto& command = getDisplayCommand(display);
         command.expectedPresentTime = expectedPresentTime;
         command.presentOrValidateDisplay = true;
+        command.frameIntervalNs = frameIntervalNs;
     }
 
     void acceptDisplayChanges(int64_t display) {
diff --git a/graphics/composer/aidl/vts/VtsComposerClient.h b/graphics/composer/aidl/vts/VtsComposerClient.h
index 20dc733..b45c71f 100644
--- a/graphics/composer/aidl/vts/VtsComposerClient.h
+++ b/graphics/composer/aidl/vts/VtsComposerClient.h
@@ -197,6 +197,7 @@
     std::vector<RefreshRateChangedDebugData> takeListOfRefreshRateChangedDebugData();
 
     static constexpr int32_t kMaxFrameIntervalNs = 50000000;  // 20fps
+    static constexpr int32_t kNoFrameIntervalNs = 0;
 
   private:
     void addDisplayConfigs(VtsDisplay*, const std::vector<DisplayConfiguration>&);
diff --git a/graphics/composer/aidl/vts/VtsHalGraphicsComposer3_ReadbackTest.cpp b/graphics/composer/aidl/vts/VtsHalGraphicsComposer3_ReadbackTest.cpp
index 466954f..58eca6e 100644
--- a/graphics/composer/aidl/vts/VtsHalGraphicsComposer3_ReadbackTest.cpp
+++ b/graphics/composer/aidl/vts/VtsHalGraphicsComposer3_ReadbackTest.cpp
@@ -220,7 +220,8 @@
 
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         // if hwc cannot handle and asks for composition change,
         // just succeed the test
@@ -279,7 +280,8 @@
 
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
 
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
@@ -338,7 +340,8 @@
                                       getDisplayHeight(), mPixelFormat, mDataspace);
         ASSERT_NO_FATAL_FAILURE(readbackBuffer.setReadbackBuffer());
 
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
 
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
@@ -465,7 +468,8 @@
         ASSERT_NO_FATAL_FAILURE(readbackBuffer.setReadbackBuffer());
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
 
         auto changedCompositionTypes = mReader.takeChangedCompositionTypes(getPrimaryDisplayId());
@@ -498,7 +502,8 @@
             mWriter->setClientTarget(getPrimaryDisplayId(), /*slot*/ 0, buffer, clientFence,
                                      clientDataspace, std::vector<common::Rect>(1, damage));
             layer->setToClientComposition(*mWriter);
-            mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+            mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                     VtsComposerClient::kNoFrameIntervalNs);
             execute();
             changedCompositionTypes = mReader.takeChangedCompositionTypes(getPrimaryDisplayId());
             ASSERT_TRUE(changedCompositionTypes.empty());
@@ -575,7 +580,8 @@
         clientLayer->setDisplayFrame(clientFrame);
         clientLayer->setZOrder(0);
         clientLayer->write(*mWriter);
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
 
         auto changedCompositionTypes = mReader.takeChangedCompositionTypes(getPrimaryDisplayId());
@@ -604,7 +610,8 @@
         mWriter->setClientTarget(getPrimaryDisplayId(), /*slot*/ 0, buffer, clientFence,
                                  clientDataspace, std::vector<common::Rect>(1, clientFrame));
         clientLayer->setToClientComposition(*mWriter);
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         changedCompositionTypes = mReader.takeChangedCompositionTypes(getPrimaryDisplayId());
         ASSERT_TRUE(changedCompositionTypes.empty());
@@ -652,7 +659,8 @@
 
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -680,7 +688,8 @@
 
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         ASSERT_TRUE(mReader.takeErrors().empty());
         ASSERT_TRUE(mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty());
@@ -721,7 +730,8 @@
 
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -785,7 +795,8 @@
         ASSERT_NO_FATAL_FAILURE(readbackBuffer.setReadbackBuffer());
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -843,7 +854,8 @@
 
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -865,7 +877,8 @@
 
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         ASSERT_TRUE(mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty());
         ASSERT_TRUE(mReader.takeErrors().empty());
@@ -930,7 +943,8 @@
 
         writeLayers(layers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED()
@@ -1065,7 +1079,8 @@
         ASSERT_NO_FATAL_FAILURE(readbackBuffer.setReadbackBuffer());
         writeLayers(mLayers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -1110,7 +1125,8 @@
         ASSERT_NO_FATAL_FAILURE(readbackBuffer.setReadbackBuffer());
         writeLayers(mLayers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -1150,7 +1166,8 @@
         ASSERT_NO_FATAL_FAILURE(readbackBuffer.setReadbackBuffer());
         writeLayers(mLayers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -1234,7 +1251,8 @@
 
         writeLayers(mLayers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -1280,7 +1298,8 @@
 
         writeLayers(mLayers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -1326,7 +1345,8 @@
 
         writeLayers(mLayers);
         ASSERT_TRUE(mReader.takeErrors().empty());
-        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                 VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED();
@@ -1419,7 +1439,8 @@
             EXPECT_TRUE(mComposerClient->setPowerMode(getPrimaryDisplayId(), PowerMode::ON).isOk());
 
             ASSERT_TRUE(mReader.takeErrors().empty());
-            mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+            mWriter->validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                     VtsComposerClient::kNoFrameIntervalNs);
             execute();
             if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
                 continue;
diff --git a/graphics/composer/aidl/vts/VtsHalGraphicsComposer3_TargetTest.cpp b/graphics/composer/aidl/vts/VtsHalGraphicsComposer3_TargetTest.cpp
index 6d74951..c135298 100644
--- a/graphics/composer/aidl/vts/VtsHalGraphicsComposer3_TargetTest.cpp
+++ b/graphics/composer/aidl/vts/VtsHalGraphicsComposer3_TargetTest.cpp
@@ -1479,7 +1479,8 @@
                                   /*acquireFence*/ -1);
             writer.setLayerDataspace(display.getDisplayId(), layer, common::Dataspace::UNKNOWN);
 
-            writer.validateDisplay(display.getDisplayId(), ComposerClientWriter::kNoTimestamp);
+            writer.validateDisplay(display.getDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                   VtsComposerClient::kNoFrameIntervalNs);
             execute();
             ASSERT_TRUE(mReader.takeErrors().empty());
 
@@ -1496,7 +1497,8 @@
                                   /*acquireFence*/ -1);
             writer.setLayerSurfaceDamage(display.getDisplayId(), layer,
                                          std::vector<Rect>(1, {0, 0, 10, 10}));
-            writer.validateDisplay(display.getDisplayId(), ComposerClientWriter::kNoTimestamp);
+            writer.validateDisplay(display.getDisplayId(), ComposerClientWriter::kNoTimestamp,
+                                   VtsComposerClient::kNoFrameIntervalNs);
             execute();
             ASSERT_TRUE(mReader.takeErrors().empty());
 
@@ -1510,7 +1512,8 @@
     sp<::android::Fence> presentAndGetFence(
             std::optional<ClockMonotonicTimestamp> expectedPresentTime) {
         auto& writer = getWriter(getPrimaryDisplayId());
-        writer.validateDisplay(getPrimaryDisplayId(), expectedPresentTime);
+        writer.validateDisplay(getPrimaryDisplayId(), expectedPresentTime,
+                               VtsComposerClient::kNoFrameIntervalNs);
         execute();
         EXPECT_TRUE(mReader.takeErrors().empty());
 
@@ -1852,20 +1855,23 @@
 
 TEST_P(GraphicsComposerAidlCommandTest, ValidDisplay) {
     auto& writer = getWriter(getPrimaryDisplayId());
-    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                           VtsComposerClient::kNoFrameIntervalNs);
     execute();
 }
 
 TEST_P(GraphicsComposerAidlCommandTest, AcceptDisplayChanges) {
     auto& writer = getWriter(getPrimaryDisplayId());
-    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                           VtsComposerClient::kNoFrameIntervalNs);
     writer.acceptDisplayChanges(getPrimaryDisplayId());
     execute();
 }
 
 TEST_P(GraphicsComposerAidlCommandTest, PresentDisplay) {
     auto& writer = getWriter(getPrimaryDisplayId());
-    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                           VtsComposerClient::kNoFrameIntervalNs);
     writer.presentDisplay(getPrimaryDisplayId());
     execute();
 }
@@ -1904,7 +1910,8 @@
         writer.setLayerBuffer(getPrimaryDisplayId(), layer, /*slot*/ 0, handle,
                               /*acquireFence*/ -1);
         writer.setLayerDataspace(getPrimaryDisplayId(), layer, Dataspace::UNKNOWN);
-        writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+        writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                               VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (!mReader.takeChangedCompositionTypes(getPrimaryDisplayId()).empty()) {
             GTEST_SUCCEED() << "Composition change requested, skipping test";
@@ -1946,7 +1953,8 @@
                    (float)getPrimaryDisplay().getDisplayHeight()};
     configureLayer(getPrimaryDisplay(), layer, Composition::CURSOR, displayFrame, cropRect);
     writer.setLayerDataspace(getPrimaryDisplayId(), layer, Dataspace::UNKNOWN);
-    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                           VtsComposerClient::kNoFrameIntervalNs);
 
     execute();
 
@@ -1961,7 +1969,8 @@
     execute();
 
     writer.setLayerCursorPosition(getPrimaryDisplayId(), layer, /*x*/ 0, /*y*/ 0);
-    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp);
+    writer.validateDisplay(getPrimaryDisplayId(), ComposerClientWriter::kNoTimestamp,
+                           VtsComposerClient::kNoFrameIntervalNs);
     writer.presentDisplay(getPrimaryDisplayId());
     execute();
 }
@@ -2160,7 +2169,8 @@
         auto& writer = getWriter(display.getDisplayId());
         writer.setLayerBuffer(display.getDisplayId(), layer, /*slot*/ 0, decorBuffer->handle,
                               /*acquireFence*/ -1);
-        writer.validateDisplay(display.getDisplayId(), ComposerClientWriter::kNoTimestamp);
+        writer.validateDisplay(display.getDisplayId(), ComposerClientWriter::kNoTimestamp,
+                               VtsComposerClient::kNoFrameIntervalNs);
         execute();
         if (support) {
             ASSERT_TRUE(mReader.takeErrors().empty());
@@ -2864,7 +2874,8 @@
         auto& reader = readers.at(displayId);
         lock.unlock();
 
-        writer.validateDisplay(displayId, ComposerClientWriter::kNoTimestamp);
+        writer.validateDisplay(displayId, ComposerClientWriter::kNoTimestamp,
+                               VtsComposerClient::kNoFrameIntervalNs);
         execute(writer, reader);
 
         threads.emplace_back([this, displayId, &readers, &readersMutex]() {
diff --git a/security/authgraph/aidl/vts/functional/Android.bp b/security/authgraph/aidl/vts/functional/Android.bp
index fc13759..0e3480f 100644
--- a/security/authgraph/aidl/vts/functional/Android.bp
+++ b/security/authgraph/aidl/vts/functional/Android.bp
@@ -46,3 +46,36 @@
         "vts",
     ],
 }
+
+rust_test {
+    name: "VtsAidlAuthGraphRoleTest",
+    srcs: ["role_test.rs"],
+    test_suites: [
+        "general-tests",
+        "vts",
+    ],
+    defaults: [
+        "authgraph_use_latest_hal_aidl_rust",
+    ],
+    rustlibs: [
+        "libauthgraph_vts_test",
+        "libbinder_rs",
+    ],
+}
+
+rust_library {
+    name: "libauthgraph_vts_test",
+    crate_name: "authgraph_vts_test",
+    srcs: ["lib.rs"],
+    defaults: [
+        "authgraph_use_latest_hal_aidl_rust",
+    ],
+    rustlibs: [
+        "libauthgraph_boringssl",
+        "libauthgraph_core",
+        "libauthgraph_hal",
+        "libauthgraph_nonsecure",
+        "libbinder_rs",
+        "libcoset",
+    ],
+}
diff --git a/security/authgraph/aidl/vts/functional/lib.rs b/security/authgraph/aidl/vts/functional/lib.rs
new file mode 100644
index 0000000..7b9b2b9
--- /dev/null
+++ b/security/authgraph/aidl/vts/functional/lib.rs
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+
+//! VTS test library for AuthGraph functionality.
+//!
+//! This test code is bundled as a library, not as `[cfg(test)]`, to allow it to be
+//! re-used inside the (Rust) VTS tests of components that use AuthGraph.
+
+use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
+    Error::Error, IAuthGraphKeyExchange::IAuthGraphKeyExchange, Identity::Identity,
+    PlainPubKey::PlainPubKey, PubKey::PubKey, SessionIdSignature::SessionIdSignature,
+};
+use authgraph_boringssl as boring;
+use authgraph_core::keyexchange as ke;
+use authgraph_core::{arc, key, traits};
+use authgraph_nonsecure::StdClock;
+use coset::CborSerializable;
+
+pub mod sink;
+pub mod source;
+
+/// Return a collection of AuthGraph trait implementations suitable for testing.
+pub fn test_impls() -> traits::TraitImpl {
+    // Note that the local implementation is using a clock with a potentially different epoch than
+    // the implementation under test.
+    boring::trait_impls(
+        Box::<boring::test_device::AgDevice>::default(),
+        Some(Box::new(StdClock::default())),
+    )
+}
+
+fn build_plain_pub_key(pub_key: &Option<Vec<u8>>) -> PubKey {
+    PubKey::PlainKey(PlainPubKey {
+        plainPubKey: pub_key.clone().unwrap(),
+    })
+}
+
+fn extract_plain_pub_key(pub_key: &Option<PubKey>) -> &PlainPubKey {
+    match pub_key {
+        Some(PubKey::PlainKey(pub_key)) => pub_key,
+        Some(PubKey::SignedKey(_)) => panic!("expect unsigned public key"),
+        None => panic!("expect pubKey to be populated"),
+    }
+}
+
+fn verification_key_from_identity(impls: &traits::TraitImpl, identity: &[u8]) -> key::EcVerifyKey {
+    let identity = key::Identity::from_slice(identity).expect("invalid identity CBOR");
+    impls
+        .device
+        .process_peer_cert_chain(&identity.cert_chain, &*impls.ecdsa)
+        .expect("failed to extract signing key")
+}
+
+fn vec_to_identity(data: &[u8]) -> Identity {
+    Identity {
+        identity: data.to_vec(),
+    }
+}
+
+fn vec_to_signature(data: &[u8]) -> SessionIdSignature {
+    SessionIdSignature {
+        signature: data.to_vec(),
+    }
+}
+
+/// Decrypt a pair of AES-256 keys encrypted with the AuthGraph PBK.
+pub fn decipher_aes_keys(imp: &traits::TraitImpl, arc: &[Vec<u8>; 2]) -> [key::AesKey; 2] {
+    [
+        decipher_aes_key(imp, &arc[0]),
+        decipher_aes_key(imp, &arc[1]),
+    ]
+}
+
+/// Decrypt an AES-256 key encrypted with the AuthGraph PBK.
+pub fn decipher_aes_key(imp: &traits::TraitImpl, arc: &[u8]) -> key::AesKey {
+    let pbk = imp.device.get_per_boot_key().expect("no PBK available");
+    let arc::ArcContent {
+        payload,
+        protected_headers: _,
+        unprotected_headers: _,
+    } = arc::decipher_arc(&pbk, arc, &*imp.aes_gcm).expect("failed to decrypt arc");
+    assert_eq!(payload.0.len(), 32);
+    let mut key = key::AesKey([0; 32]);
+    key.0.copy_from_slice(&payload.0);
+    assert_ne!(key.0, [0; 32], "agreed AES-256 key should be non-zero");
+    key
+}
diff --git a/security/authgraph/aidl/vts/functional/role_test.rs b/security/authgraph/aidl/vts/functional/role_test.rs
new file mode 100644
index 0000000..e95361a
--- /dev/null
+++ b/security/authgraph/aidl/vts/functional/role_test.rs
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+//! Tests of individual AuthGraph role (source or sink) functionality.
+
+#![cfg(test)]
+
+use authgraph_vts_test as vts;
+use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
+    IAuthGraphKeyExchange::IAuthGraphKeyExchange,
+};
+
+const AUTH_GRAPH_NONSECURE: &str =
+    "android.hardware.security.authgraph.IAuthGraphKeyExchange/nonsecure";
+
+/// Retrieve the /nonsecure instance of AuthGraph, which supports both sink and source roles.
+fn get_nonsecure() -> Option<binder::Strong<dyn IAuthGraphKeyExchange>> {
+    binder::get_interface(AUTH_GRAPH_NONSECURE).ok()
+}
+
+/// Macro to require availability of a /nonsecure instance of AuthGraph.
+///
+/// Note that this macro triggers `return` if not found.
+macro_rules! require_nonsecure {
+    {} => {
+        match get_nonsecure() {
+            Some(v) => v,
+            None => {
+                eprintln!("Skipping test as no /nonsecure impl found");
+                return;
+            }
+        }
+    }
+}
+
+#[test]
+fn test_nonsecure_source_mainline() {
+    let mut impls = vts::test_impls();
+    vts::source::test_mainline(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_source_corrupt_sig() {
+    let mut impls = vts::test_impls();
+    vts::source::test_corrupt_sig(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_source_corrupt_keys() {
+    let mut impls = vts::test_impls();
+    vts::source::test_corrupt_key(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_sink_mainline() {
+    let mut impls = vts::test_impls();
+    vts::sink::test_mainline(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_sink_corrupt_sig() {
+    let mut impls = vts::test_impls();
+    vts::sink::test_corrupt_sig(&mut impls, require_nonsecure!());
+}
+#[test]
+fn test_nonsecure_sink_corrupt_keys() {
+    let mut impls = vts::test_impls();
+    vts::sink::test_corrupt_keys(&mut impls, require_nonsecure!());
+}
diff --git a/security/authgraph/aidl/vts/functional/sink.rs b/security/authgraph/aidl/vts/functional/sink.rs
new file mode 100644
index 0000000..5c81593
--- /dev/null
+++ b/security/authgraph/aidl/vts/functional/sink.rs
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ */
+
+//! VTS tests for sinks
+use super::*;
+use authgraph_core::traits;
+
+/// Run AuthGraph tests against the provided sink, using a local test source implementation.
+pub fn test(impls: &mut traits::TraitImpl, sink: binder::Strong<dyn IAuthGraphKeyExchange>) {
+    test_mainline(impls, sink.clone());
+    test_corrupt_sig(impls, sink.clone());
+    test_corrupt_keys(impls, sink);
+}
+
+/// Perform mainline AuthGraph key exchange with the provided sink and local implementation.
+/// Return the agreed AES keys in plaintext.
+pub fn test_mainline(
+    impls: &mut traits::TraitImpl,
+    sink: binder::Strong<dyn IAuthGraphKeyExchange>,
+) -> [key::AesKey; 2] {
+    // Step 1: create an ephemeral ECDH key at the (local) source.
+    let source_init_info = ke::create(impls).expect("failed to create() with local impl");
+
+    // Step 2: pass the source's ECDH public key and other session info to the (remote) sink.
+    let init_result = sink
+        .init(
+            &build_plain_pub_key(&source_init_info.ke_key.pub_key),
+            &vec_to_identity(&source_init_info.identity),
+            &source_init_info.nonce,
+            source_init_info.version,
+        )
+        .expect("failed to init() with remote impl");
+    let sink_init_info = init_result.sessionInitiationInfo;
+    let sink_pub_key = extract_plain_pub_key(&sink_init_info.key.pubKey);
+
+    let sink_info = init_result.sessionInfo;
+    assert!(!sink_info.sessionId.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let sink_verification_key =
+        verification_key_from_identity(&impls, &sink_init_info.identity.identity);
+    ke::verify_signature_on_session_id(
+        &sink_verification_key,
+        &sink_info.sessionId,
+        &sink_info.signature.signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (local) source, so it
+    // can calculate the same pair of symmetric keys.
+    let source_info = ke::finish(
+        impls,
+        &sink_pub_key.plainPubKey,
+        &sink_init_info.identity.identity,
+        &sink_info.signature.signature,
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        source_init_info.ke_key,
+    )
+    .expect("failed to finish() with local impl");
+    assert!(!source_info.session_id.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let source_verification_key =
+        verification_key_from_identity(&impls, &source_init_info.identity);
+    ke::verify_signature_on_session_id(
+        &source_verification_key,
+        &source_info.session_id,
+        &source_info.session_id_signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Both ends should agree on the session ID.
+    assert_eq!(source_info.session_id, sink_info.sessionId);
+
+    // Step 4: pass the (local) source's session ID signature back to the sink, so it can check it
+    // and update the symmetric keys so they're marked as authentication complete.
+    let _sink_arcs = sink
+        .authenticationComplete(
+            &vec_to_signature(&source_info.session_id_signature),
+            &sink_info.sharedKeys,
+        )
+        .expect("failed to authenticationComplete() with remote sink");
+
+    // Decrypt and return the session keys.
+    decipher_aes_keys(&impls, &source_info.shared_keys)
+}
+
+/// Perform mainline AuthGraph key exchange with the provided sink, but provide an invalid
+/// session ID signature.
+pub fn test_corrupt_sig(
+    impls: &mut traits::TraitImpl,
+    sink: binder::Strong<dyn IAuthGraphKeyExchange>,
+) {
+    // Step 1: create an ephemeral ECDH key at the (local) source.
+    let source_init_info = ke::create(impls).expect("failed to create() with local impl");
+
+    // Step 2: pass the source's ECDH public key and other session info to the (remote) sink.
+    let init_result = sink
+        .init(
+            &build_plain_pub_key(&source_init_info.ke_key.pub_key),
+            &vec_to_identity(&source_init_info.identity),
+            &source_init_info.nonce,
+            source_init_info.version,
+        )
+        .expect("failed to init() with remote impl");
+    let sink_init_info = init_result.sessionInitiationInfo;
+    let sink_pub_key = extract_plain_pub_key(&sink_init_info.key.pubKey);
+
+    let sink_info = init_result.sessionInfo;
+    assert!(!sink_info.sessionId.is_empty());
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (local) source, so it
+    // can calculate the same pair of symmetric keys.
+    let source_info = ke::finish(
+        impls,
+        &sink_pub_key.plainPubKey,
+        &sink_init_info.identity.identity,
+        &sink_info.signature.signature,
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        source_init_info.ke_key,
+    )
+    .expect("failed to finish() with local impl");
+    assert!(!source_info.session_id.is_empty());
+
+    // Build a corrupted version of the (local) source's session ID signature.
+    let mut corrupt_signature = source_info.session_id_signature.clone();
+    let sig_len = corrupt_signature.len();
+    corrupt_signature[sig_len - 1] ^= 0x01;
+
+    // Step 4: pass the (local) source's **invalid** session ID signature back to the sink,
+    // which should reject it.
+    let result =
+        sink.authenticationComplete(&vec_to_signature(&corrupt_signature), &sink_info.sharedKeys);
+    let err = result.expect_err("expect failure with corrupt signature");
+    assert_eq!(
+        err,
+        binder::Status::new_service_specific_error(Error::INVALID_SIGNATURE.0, None)
+    );
+}
+
+/// Perform mainline AuthGraph key exchange with the provided sink, but provide an invalid
+/// Arc for the sink's key.
+pub fn test_corrupt_keys(
+    impls: &mut traits::TraitImpl,
+    sink: binder::Strong<dyn IAuthGraphKeyExchange>,
+) {
+    // Step 1: create an ephemeral ECDH key at the (local) source.
+    let source_init_info = ke::create(impls).expect("failed to create() with local impl");
+
+    // Step 2: pass the source's ECDH public key and other session info to the (remote) sink.
+    let init_result = sink
+        .init(
+            &build_plain_pub_key(&source_init_info.ke_key.pub_key),
+            &vec_to_identity(&source_init_info.identity),
+            &source_init_info.nonce,
+            source_init_info.version,
+        )
+        .expect("failed to init() with remote impl");
+    let sink_init_info = init_result.sessionInitiationInfo;
+    let sink_pub_key = extract_plain_pub_key(&sink_init_info.key.pubKey);
+
+    let sink_info = init_result.sessionInfo;
+    assert!(!sink_info.sessionId.is_empty());
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (local) source, so it
+    // can calculate the same pair of symmetric keys.
+    let source_info = ke::finish(
+        impls,
+        &sink_pub_key.plainPubKey,
+        &sink_init_info.identity.identity,
+        &sink_info.signature.signature,
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        source_init_info.ke_key,
+    )
+    .expect("failed to finish() with local impl");
+    assert!(!source_info.session_id.is_empty());
+
+    // Deliberately corrupt the sink's shared key Arcs before returning them
+    let mut corrupt_keys = sink_info.sharedKeys.clone();
+    let len0 = corrupt_keys[0].arc.len();
+    let len1 = corrupt_keys[1].arc.len();
+    corrupt_keys[0].arc[len0 - 1] ^= 0x01;
+    corrupt_keys[1].arc[len1 - 1] ^= 0x01;
+
+    // Step 4: pass the (local) source's session ID signature back to the sink, but with corrupted
+    // keys, which should be rejected.
+    let result = sink.authenticationComplete(
+        &vec_to_signature(&source_info.session_id_signature),
+        &corrupt_keys,
+    );
+    let err = result.expect_err("expect failure with corrupt keys");
+    assert_eq!(
+        err,
+        binder::Status::new_service_specific_error(Error::INVALID_SHARED_KEY_ARCS.0, None)
+    );
+}
diff --git a/security/authgraph/aidl/vts/functional/source.rs b/security/authgraph/aidl/vts/functional/source.rs
new file mode 100644
index 0000000..9aaaaee
--- /dev/null
+++ b/security/authgraph/aidl/vts/functional/source.rs
@@ -0,0 +1,244 @@
+/*
+ * 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.
+ */
+
+//! VTS tests for sources
+use super::*;
+use authgraph_core::traits;
+
+/// Run AuthGraph tests against the provided source, using a local test sink implementation.
+pub fn test(impls: &mut traits::TraitImpl, source: binder::Strong<dyn IAuthGraphKeyExchange>) {
+    test_mainline(impls, source.clone());
+    test_corrupt_sig(impls, source.clone());
+    test_corrupt_key(impls, source);
+}
+
+/// Perform mainline AuthGraph key exchange with the provided source.
+/// Return the agreed AES keys in plaintext.
+pub fn test_mainline(
+    impls: &mut traits::TraitImpl,
+    source: binder::Strong<dyn IAuthGraphKeyExchange>,
+) -> [key::AesKey; 2] {
+    // Step 1: create an ephemeral ECDH key at the (remote) source.
+    let source_init_info = source
+        .create()
+        .expect("failed to create() with remote impl");
+    assert!(source_init_info.key.pubKey.is_some());
+    assert!(source_init_info.key.arcFromPBK.is_some());
+    let source_pub_key = extract_plain_pub_key(&source_init_info.key.pubKey);
+
+    // Step 2: pass the source's ECDH public key and other session info to the (local) sink.
+    let init_result = ke::init(
+        impls,
+        &source_pub_key.plainPubKey,
+        &source_init_info.identity.identity,
+        &source_init_info.nonce,
+        source_init_info.version,
+    )
+    .expect("failed to init() with local impl");
+    let sink_init_info = init_result.session_init_info;
+    let sink_pub_key = sink_init_info
+        .ke_key
+        .pub_key
+        .expect("expect pub_key to be populated");
+
+    let sink_info = init_result.session_info;
+    assert!(!sink_info.session_id.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let sink_verification_key = verification_key_from_identity(&impls, &sink_init_info.identity);
+    ke::verify_signature_on_session_id(
+        &sink_verification_key,
+        &sink_info.session_id,
+        &sink_info.session_id_signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (remote) source, so it
+    // can calculate the same pair of symmetric keys.
+    let source_info = source
+        .finish(
+            &PubKey::PlainKey(PlainPubKey {
+                plainPubKey: sink_pub_key,
+            }),
+            &Identity {
+                identity: sink_init_info.identity,
+            },
+            &vec_to_signature(&sink_info.session_id_signature),
+            &sink_init_info.nonce,
+            sink_init_info.version,
+            &source_init_info.key,
+        )
+        .expect("failed to finish() with remote impl");
+    assert!(!source_info.sessionId.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let source_verification_key =
+        verification_key_from_identity(&impls, &source_init_info.identity.identity);
+    ke::verify_signature_on_session_id(
+        &source_verification_key,
+        &source_info.sessionId,
+        &source_info.signature.signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Both ends should agree on the session ID.
+    assert_eq!(source_info.sessionId, sink_info.session_id);
+
+    // Step 4: pass the (remote) source's session ID signature back to the sink, so it can check it
+    // and update the symmetric keys so they're marked as authentication complete.
+    let sink_arcs = ke::authentication_complete(
+        impls,
+        &source_info.signature.signature,
+        sink_info.shared_keys,
+    )
+    .expect("failed to authenticationComplete() with local sink");
+
+    // Decrypt and return the session keys.
+    decipher_aes_keys(&impls, &sink_arcs)
+}
+
+/// Perform mainline AuthGraph key exchange with the provided source, but provide an invalid session
+/// ID signature.
+pub fn test_corrupt_sig(
+    impls: &mut traits::TraitImpl,
+    source: binder::Strong<dyn IAuthGraphKeyExchange>,
+) {
+    // Step 1: create an ephemeral ECDH key at the (remote) source.
+    let source_init_info = source
+        .create()
+        .expect("failed to create() with remote impl");
+    assert!(source_init_info.key.pubKey.is_some());
+    assert!(source_init_info.key.arcFromPBK.is_some());
+    let source_pub_key = extract_plain_pub_key(&source_init_info.key.pubKey);
+
+    // Step 2: pass the source's ECDH public key and other session info to the (local) sink.
+    let init_result = ke::init(
+        impls,
+        &source_pub_key.plainPubKey,
+        &source_init_info.identity.identity,
+        &source_init_info.nonce,
+        source_init_info.version,
+    )
+    .expect("failed to init() with local impl");
+    let sink_init_info = init_result.session_init_info;
+    let sink_pub_key = sink_init_info
+        .ke_key
+        .pub_key
+        .expect("expect pub_key to be populated");
+    let sink_info = init_result.session_info;
+    assert!(!sink_info.session_id.is_empty());
+
+    // Deliberately corrupt the sink's session ID signature.
+    let mut corrupt_signature = sink_info.session_id_signature.clone();
+    let sig_len = corrupt_signature.len();
+    corrupt_signature[sig_len - 1] ^= 0x01;
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (remote) source, so it
+    // can calculate the same pair of symmetric keys.
+    let result = source.finish(
+        &PubKey::PlainKey(PlainPubKey {
+            plainPubKey: sink_pub_key,
+        }),
+        &Identity {
+            identity: sink_init_info.identity,
+        },
+        &vec_to_signature(&corrupt_signature),
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        &source_init_info.key,
+    );
+    let err = result.expect_err("expect failure with corrupt signature");
+    assert_eq!(
+        err,
+        binder::Status::new_service_specific_error(Error::INVALID_SIGNATURE.0, None)
+    );
+}
+
+/// Perform mainline AuthGraph key exchange with the provided source, but give it back
+/// a corrupted key.
+pub fn test_corrupt_key(
+    impls: &mut traits::TraitImpl,
+    source: binder::Strong<dyn IAuthGraphKeyExchange>,
+) {
+    // Step 1: create an ephemeral ECDH key at the (remote) source.
+    let source_init_info = source
+        .create()
+        .expect("failed to create() with remote impl");
+    assert!(source_init_info.key.pubKey.is_some());
+    assert!(source_init_info.key.arcFromPBK.is_some());
+    let source_pub_key = extract_plain_pub_key(&source_init_info.key.pubKey);
+
+    // Step 2: pass the source's ECDH public key and other session info to the (local) sink.
+    let init_result = ke::init(
+        impls,
+        &source_pub_key.plainPubKey,
+        &source_init_info.identity.identity,
+        &source_init_info.nonce,
+        source_init_info.version,
+    )
+    .expect("failed to init() with local impl");
+    let sink_init_info = init_result.session_init_info;
+    let sink_pub_key = sink_init_info
+        .ke_key
+        .pub_key
+        .expect("expect pub_key to be populated");
+
+    let sink_info = init_result.session_info;
+    assert!(!sink_info.session_id.is_empty());
+
+    // The AuthGraph core library will verify the session ID signature, but do it here too.
+    let sink_verification_key = verification_key_from_identity(&impls, &sink_init_info.identity);
+    ke::verify_signature_on_session_id(
+        &sink_verification_key,
+        &sink_info.session_id,
+        &sink_info.session_id_signature,
+        &*impls.ecdsa,
+    )
+    .expect("failed verification of signed session ID");
+
+    // Deliberately corrupt the source's encrypted key.
+    let mut corrupt_key = source_init_info.key.clone();
+    match &mut corrupt_key.arcFromPBK {
+        Some(a) => {
+            let len = a.arc.len();
+            a.arc[len - 1] ^= 0x01;
+        }
+        None => panic!("no arc data"),
+    }
+
+    // Step 3: pass the sink's ECDH public key and other session info to the (remote) source, but
+    // give it back a corrupted version of its own key.
+    let result = source.finish(
+        &PubKey::PlainKey(PlainPubKey {
+            plainPubKey: sink_pub_key,
+        }),
+        &Identity {
+            identity: sink_init_info.identity,
+        },
+        &vec_to_signature(&sink_info.session_id_signature),
+        &sink_init_info.nonce,
+        sink_init_info.version,
+        &corrupt_key,
+    );
+
+    let err = result.expect_err("expect failure with corrupt signature");
+    assert_eq!(
+        err,
+        binder::Status::new_service_specific_error(Error::INVALID_PRIV_KEY_ARC_IN_KEY.0, None)
+    );
+}
diff --git a/security/authgraph/default/Android.bp b/security/authgraph/default/Android.bp
index 9de3bc1..c481075 100644
--- a/security/authgraph/default/Android.bp
+++ b/security/authgraph/default/Android.bp
@@ -22,6 +22,26 @@
     default_applicable_licenses: ["hardware_interfaces_license"],
 }
 
+rust_library {
+    name: "libauthgraph_nonsecure",
+    crate_name: "authgraph_nonsecure",
+    defaults: [
+        "authgraph_use_latest_hal_aidl_rust",
+    ],
+    vendor_available: true,
+    rustlibs: [
+        "libandroid_logger",
+        "libauthgraph_boringssl",
+        "libauthgraph_core",
+        "libauthgraph_hal",
+        "libbinder_rs",
+        "liblibc",
+        "liblog_rust",
+    ],
+    srcs: ["src/lib.rs"],
+
+}
+
 rust_binary {
     name: "android.hardware.security.authgraph-service.nonsecure",
     relative_install_path: "hw",
@@ -33,9 +53,8 @@
     ],
     rustlibs: [
         "libandroid_logger",
-        "libauthgraph_core",
-        "libauthgraph_boringssl",
         "libauthgraph_hal",
+        "libauthgraph_nonsecure",
         "libbinder_rs",
         "liblibc",
         "liblog_rust",
@@ -44,3 +63,20 @@
         "src/main.rs",
     ],
 }
+
+rust_fuzz {
+    name: "android.hardware.authgraph-service.nonsecure_fuzzer",
+    rustlibs: [
+        "libauthgraph_hal",
+        "libauthgraph_nonsecure",
+        "libbinder_random_parcel_rs",
+        "libbinder_rs",
+    ],
+    srcs: ["src/fuzzer.rs"],
+    fuzz_config: {
+        cc: [
+            "drysdale@google.com",
+            "hasinitg@google.com",
+        ],
+    },
+}
diff --git a/security/authgraph/default/src/fuzzer.rs b/security/authgraph/default/src/fuzzer.rs
new file mode 100644
index 0000000..6a9cfdd
--- /dev/null
+++ b/security/authgraph/default/src/fuzzer.rs
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+#![allow(missing_docs)]
+#![no_main]
+extern crate libfuzzer_sys;
+
+use authgraph_hal::service::AuthGraphService;
+use authgraph_nonsecure::LocalTa;
+use binder_random_parcel_rs::fuzz_service;
+use libfuzzer_sys::fuzz_target;
+use std::sync::{Arc, Mutex};
+
+fuzz_target!(|data: &[u8]| {
+    let local_ta = LocalTa::new();
+    let service = AuthGraphService::new_as_binder(Arc::new(Mutex::new(local_ta)));
+    fuzz_service(&mut service.as_binder(), data);
+});
diff --git a/security/authgraph/default/src/lib.rs b/security/authgraph/default/src/lib.rs
new file mode 100644
index 0000000..4cd0cb7
--- /dev/null
+++ b/security/authgraph/default/src/lib.rs
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+//! Common functionality for non-secure/testing instance of AuthGraph.
+
+use authgraph_boringssl as boring;
+use authgraph_core::{
+    key::MillisecondsSinceEpoch,
+    ta::{AuthGraphTa, Role},
+    traits,
+};
+use authgraph_hal::channel::SerializedChannel;
+use std::sync::{Arc, Mutex};
+use std::time::Instant;
+
+/// Monotonic clock with an epoch that starts at the point of construction.
+/// (This makes it unsuitable for use outside of testing, because the epoch
+/// will not match that of any other component.)
+pub struct StdClock(Instant);
+
+impl Default for StdClock {
+    fn default() -> Self {
+        Self(Instant::now())
+    }
+}
+
+impl traits::MonotonicClock for StdClock {
+    fn now(&self) -> MillisecondsSinceEpoch {
+        let millis: i64 = self
+            .0
+            .elapsed()
+            .as_millis()
+            .try_into()
+            .expect("failed to fit timestamp in i64");
+        MillisecondsSinceEpoch(millis)
+    }
+}
+
+/// Implementation of the AuthGraph TA that runs locally in-process (and which is therefore
+/// insecure).
+pub struct LocalTa {
+    ta: Arc<Mutex<AuthGraphTa>>,
+}
+
+impl LocalTa {
+    /// Create a new instance.
+    pub fn new() -> Self {
+        Self {
+            ta: Arc::new(Mutex::new(AuthGraphTa::new(
+                boring::trait_impls(
+                    Box::<boring::test_device::AgDevice>::default(),
+                    Some(Box::new(StdClock::default())),
+                ),
+                Role::Both,
+            ))),
+        }
+    }
+}
+
+/// Pretend to be a serialized channel to the TA, but actually just directly invoke the TA with
+/// incoming requests.
+impl SerializedChannel for LocalTa {
+    const MAX_SIZE: usize = usize::MAX;
+
+    fn execute(&mut self, req_data: &[u8]) -> binder::Result<Vec<u8>> {
+        Ok(self.ta.lock().unwrap().process(req_data))
+    }
+}
diff --git a/security/authgraph/default/src/main.rs b/security/authgraph/default/src/main.rs
index 2112e58..873eb4e 100644
--- a/security/authgraph/default/src/main.rs
+++ b/security/authgraph/default/src/main.rs
@@ -22,18 +22,10 @@
 //! expose an entrypoint that allowed retrieval of the specific IAuthGraphKeyExchange instance that
 //! is correlated with the component).
 
-use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
-    Arc::Arc, IAuthGraphKeyExchange::BnAuthGraphKeyExchange,
-    IAuthGraphKeyExchange::IAuthGraphKeyExchange, Identity::Identity, KeInitResult::KeInitResult,
-    Key::Key, PubKey::PubKey, SessionIdSignature::SessionIdSignature, SessionInfo::SessionInfo,
-    SessionInitiationInfo::SessionInitiationInfo,
-};
-use authgraph_boringssl as boring;
-use authgraph_core::{key::MillisecondsSinceEpoch, keyexchange as ke, traits};
-use authgraph_hal::{err_to_binder, Innto, TryInnto};
+use authgraph_hal::service;
+use authgraph_nonsecure::LocalTa;
 use log::{error, info};
-use std::ffi::CString;
-use std::sync::Mutex;
+use std::sync::{Arc, Mutex};
 
 static SERVICE_NAME: &str = "android.hardware.security.authgraph.IAuthGraphKeyExchange";
 static SERVICE_INSTANCE: &str = "nonsecure";
@@ -73,7 +65,8 @@
     binder::ProcessState::start_thread_pool();
 
     // Register the service
-    let service = AuthGraphService::new_as_binder();
+    let local_ta = LocalTa::new();
+    let service = service::AuthGraphService::new_as_binder(Arc::new(Mutex::new(local_ta)));
     let service_name = format!("{}/{}", SERVICE_NAME, SERVICE_INSTANCE);
     binder::add_service(&service_name, service.as_binder()).map_err(|e| {
         format!(
@@ -87,141 +80,3 @@
     info!("AuthGraph HAL service is terminating."); // should not reach here
     Ok(())
 }
-
-/// Non-secure implementation of the AuthGraph key exchange service.
-struct AuthGraphService {
-    imp: Mutex<traits::TraitImpl>,
-}
-
-impl AuthGraphService {
-    /// Create a new instance.
-    fn new() -> Self {
-        Self {
-            imp: Mutex::new(traits::TraitImpl {
-                aes_gcm: Box::new(boring::BoringAes),
-                ecdh: Box::new(boring::BoringEcDh),
-                ecdsa: Box::new(boring::BoringEcDsa),
-                hmac: Box::new(boring::BoringHmac),
-                hkdf: Box::new(boring::BoringHkdf),
-                sha256: Box::new(boring::BoringSha256),
-                rng: Box::new(boring::BoringRng),
-                device: Box::<boring::test_device::AgDevice>::default(),
-                clock: Some(Box::new(StdClock)),
-            }),
-        }
-    }
-
-    /// Create a new instance wrapped in a proxy object.
-    pub fn new_as_binder() -> binder::Strong<dyn IAuthGraphKeyExchange> {
-        BnAuthGraphKeyExchange::new_binder(Self::new(), binder::BinderFeatures::default())
-    }
-}
-
-impl binder::Interface for AuthGraphService {}
-
-/// Extract (and require) an unsigned public key as bytes from a [`PubKey`].
-fn unsigned_pub_key(pub_key: &PubKey) -> binder::Result<&[u8]> {
-    match pub_key {
-        PubKey::PlainKey(key) => Ok(&key.plainPubKey),
-        PubKey::SignedKey(_) => Err(binder::Status::new_exception(
-            binder::ExceptionCode::ILLEGAL_ARGUMENT,
-            Some(&CString::new("expected unsigned public key").unwrap()),
-        )),
-    }
-}
-
-/// This nonsecure implementation of the AuthGraph HAL interface directly calls the AuthGraph
-/// reference implementation library code; a real implementation requires the AuthGraph
-/// code to run in a secure environment, not within Android.
-impl IAuthGraphKeyExchange for AuthGraphService {
-    fn create(&self) -> binder::Result<SessionInitiationInfo> {
-        info!("create()");
-        let mut imp = self.imp.lock().unwrap();
-        let info = ke::create(&mut *imp).map_err(err_to_binder)?;
-        Ok(info.innto())
-    }
-    fn init(
-        &self,
-        peer_pub_key: &PubKey,
-        peer_id: &Identity,
-        peer_nonce: &[u8],
-        peer_version: i32,
-    ) -> binder::Result<KeInitResult> {
-        info!("init(v={peer_version})");
-        let mut imp = self.imp.lock().unwrap();
-        let peer_pub_key = unsigned_pub_key(peer_pub_key)?;
-        let result = ke::init(
-            &mut *imp,
-            peer_pub_key,
-            &peer_id.identity,
-            &peer_nonce,
-            peer_version,
-        )
-        .map_err(err_to_binder)?;
-        Ok(result.innto())
-    }
-
-    fn finish(
-        &self,
-        peer_pub_key: &PubKey,
-        peer_id: &Identity,
-        peer_signature: &SessionIdSignature,
-        peer_nonce: &[u8],
-        peer_version: i32,
-        own_key: &Key,
-    ) -> binder::Result<SessionInfo> {
-        info!("finish(v={peer_version})");
-        let mut imp = self.imp.lock().unwrap();
-        let peer_pub_key = unsigned_pub_key(peer_pub_key)?;
-        let own_key: Key = own_key.clone();
-        let own_key: authgraph_core::key::Key = own_key.try_innto()?;
-        let session_info = ke::finish(
-            &mut *imp,
-            peer_pub_key,
-            &peer_id.identity,
-            &peer_signature.signature,
-            &peer_nonce,
-            peer_version,
-            own_key,
-        )
-        .map_err(err_to_binder)?;
-        Ok(session_info.innto())
-    }
-
-    fn authenticationComplete(
-        &self,
-        peer_signature: &SessionIdSignature,
-        shared_keys: &[Arc; 2],
-    ) -> binder::Result<[Arc; 2]> {
-        info!("authComplete()");
-        let mut imp = self.imp.lock().unwrap();
-        let shared_keys = [shared_keys[0].arc.clone(), shared_keys[1].arc.clone()];
-        let arcs = ke::authentication_complete(&mut *imp, &peer_signature.signature, shared_keys)
-            .map_err(err_to_binder)?;
-        Ok(arcs.map(|arc| Arc { arc }))
-    }
-}
-
-/// Monotonic clock.
-#[derive(Default)]
-pub struct StdClock;
-
-impl traits::MonotonicClock for StdClock {
-    fn now(&self) -> authgraph_core::key::MillisecondsSinceEpoch {
-        let mut time = libc::timespec {
-            tv_sec: 0,  // libc::time_t
-            tv_nsec: 0, // libc::c_long
-        };
-        let rc =
-        // Safety: `time` is a valid structure.
-            unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, &mut time as *mut libc::timespec) };
-        if rc < 0 {
-            log::warn!("failed to get time!");
-            return MillisecondsSinceEpoch(0);
-        }
-        // The types in `libc::timespec` may be different on different architectures,
-        // so allow conversion to `i64`.
-        #[allow(clippy::unnecessary_cast)]
-        MillisecondsSinceEpoch((time.tv_sec as i64 * 1000) + (time.tv_nsec as i64 / 1000 / 1000))
-    }
-}
diff --git a/tv/tuner/aidl/default/Android.bp b/tv/tuner/aidl/default/Android.bp
index 65fa821..ed97d9c 100644
--- a/tv/tuner/aidl/default/Android.bp
+++ b/tv/tuner/aidl/default/Android.bp
@@ -23,6 +23,7 @@
         "TimeFilter.cpp",
         "Tuner.cpp",
         "service.cpp",
+        "dtv_plugin.cpp",
     ],
     static_libs: [
         "libaidlcommonsupport",
diff --git a/tv/tuner/aidl/default/Demux.cpp b/tv/tuner/aidl/default/Demux.cpp
index 11e7131..34e3442 100644
--- a/tv/tuner/aidl/default/Demux.cpp
+++ b/tv/tuner/aidl/default/Demux.cpp
@@ -20,7 +20,9 @@
 #include <aidl/android/hardware/tv/tuner/DemuxQueueNotifyBits.h>
 #include <aidl/android/hardware/tv/tuner/Result.h>
 
+#include <fmq/AidlMessageQueue.h>
 #include <utils/Log.h>
+#include <thread>
 #include "Demux.h"
 
 namespace aidl {
@@ -29,6 +31,15 @@
 namespace tv {
 namespace tuner {
 
+using ::aidl::android::hardware::common::fmq::MQDescriptor;
+using ::aidl::android::hardware::common::fmq::SynchronizedReadWrite;
+using ::android::AidlMessageQueue;
+using ::android::hardware::EventFlag;
+
+using FilterMQ = AidlMessageQueue<int8_t, SynchronizedReadWrite>;
+using AidlMQ = AidlMessageQueue<int8_t, SynchronizedReadWrite>;
+using AidlMQDesc = MQDescriptor<int8_t, SynchronizedReadWrite>;
+
 #define WAIT_TIMEOUT 3000000000
 
 Demux::Demux(int32_t demuxId, uint32_t filterTypes) {
@@ -45,6 +56,111 @@
     close();
 }
 
+::ndk::ScopedAStatus Demux::openDvr(DvrType in_type, int32_t in_bufferSize,
+                                    const std::shared_ptr<IDvrCallback>& in_cb,
+                                    std::shared_ptr<IDvr>* _aidl_return) {
+    ALOGV("%s", __FUNCTION__);
+
+    if (in_cb == nullptr) {
+        ALOGW("[Demux] DVR callback can't be null");
+        *_aidl_return = nullptr;
+        return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                static_cast<int32_t>(Result::INVALID_ARGUMENT));
+    }
+
+    set<int64_t>::iterator it;
+    switch (in_type) {
+        case DvrType::PLAYBACK:
+            mDvrPlayback = ndk::SharedRefBase::make<Dvr>(in_type, in_bufferSize, in_cb,
+                                                         this->ref<Demux>());
+            if (!mDvrPlayback->createDvrMQ()) {
+                ALOGE("[Demux] cannot create dvr message queue");
+                mDvrPlayback = nullptr;
+                *_aidl_return = mDvrPlayback;
+                return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                        static_cast<int32_t>(Result::UNKNOWN_ERROR));
+            }
+
+            for (it = mPlaybackFilterIds.begin(); it != mPlaybackFilterIds.end(); it++) {
+                if (!mDvrPlayback->addPlaybackFilter(*it, mFilters[*it])) {
+                    ALOGE("[Demux] Can't get filter info for DVR playback");
+                    mDvrPlayback = nullptr;
+                    *_aidl_return = mDvrPlayback;
+                    return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                            static_cast<int32_t>(Result::UNKNOWN_ERROR));
+                }
+            }
+
+            ALOGI("Playback normal case");
+
+            *_aidl_return = mDvrPlayback;
+            return ::ndk::ScopedAStatus::ok();
+        case DvrType::RECORD:
+            mDvrRecord = ndk::SharedRefBase::make<Dvr>(in_type, in_bufferSize, in_cb,
+                                                       this->ref<Demux>());
+            if (!mDvrRecord->createDvrMQ()) {
+                mDvrRecord = nullptr;
+                *_aidl_return = mDvrRecord;
+                return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                        static_cast<int32_t>(Result::UNKNOWN_ERROR));
+            }
+
+            *_aidl_return = mDvrRecord;
+            return ::ndk::ScopedAStatus::ok();
+        default:
+            *_aidl_return = nullptr;
+            return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                    static_cast<int32_t>(Result::INVALID_ARGUMENT));
+    }
+}
+
+void Demux::readIptvThreadLoop(dtv_plugin* interface, dtv_streamer* streamer, void* buf,
+                               size_t buf_size, int timeout_ms, int buffer_timeout) {
+    Timer *timer, *fullBufferTimer;
+    while (mDemuxIptvReadThreadRunning) {
+        if (mIsIptvDvrFMQFull && fullBufferTimer->get_elapsed_time_ms() > buffer_timeout) {
+            ALOGE("DVR FMQ has not been flushed within timeout of %d ms", buffer_timeout);
+            delete fullBufferTimer;
+            break;
+        }
+        timer = new Timer();
+        ssize_t bytes_read = interface->read_stream(streamer, buf, buf_size, timeout_ms);
+        if (bytes_read == 0) {
+            double elapsed_time = timer->get_elapsed_time_ms();
+            if (elapsed_time > timeout_ms) {
+                ALOGE("[Demux] timeout reached - elapsed_time: %f, timeout: %d", elapsed_time,
+                      timeout_ms);
+            }
+            ALOGE("[Demux] Cannot read data from the socket");
+            delete timer;
+            break;
+        }
+
+        delete timer;
+        ALOGI("Number of bytes read: %zd", bytes_read);
+        int result = mDvrPlayback->writePlaybackFMQ(buf, bytes_read);
+
+        switch (result) {
+            case DVR_WRITE_FAILURE_REASON_FMQ_FULL:
+                if (!mIsIptvDvrFMQFull) {
+                    mIsIptvDvrFMQFull = true;
+                    fullBufferTimer = new Timer();
+                }
+                ALOGI("Waiting for client to flush DVR FMQ.");
+                break;
+            case DVR_WRITE_FAILURE_REASON_UNKNOWN:
+                ALOGE("Failed to write data into DVR FMQ for unknown reason");
+                break;
+            case DVR_WRITE_SUCCESS:
+                ALOGI("Wrote %zd bytes to DVR FMQ", bytes_read);
+                break;
+            default:
+                ALOGI("Invalid DVR Status");
+        }
+    }
+    mDemuxIptvReadThreadRunning = false;
+}
+
 ::ndk::ScopedAStatus Demux::setFrontendDataSource(int32_t in_frontendId) {
     ALOGV("%s", __FUNCTION__);
 
@@ -52,7 +168,6 @@
         return ::ndk::ScopedAStatus::fromServiceSpecificError(
                 static_cast<int32_t>(Result::NOT_INITIALIZED));
     }
-
     mFrontend = mTuner->getFrontendById(in_frontendId);
     if (mFrontend == nullptr) {
         return ::ndk::ScopedAStatus::fromServiceSpecificError(
@@ -61,6 +176,58 @@
 
     mTuner->setFrontendAsDemuxSource(in_frontendId, mDemuxId);
 
+    // if mFrontend is an IPTV frontend, create streamer to read TS data from socket
+    if (mFrontend->getFrontendType() == FrontendType::IPTV) {
+        // create a DVR instance on the demux
+        shared_ptr<IDvr> iptvDvr;
+
+        std::shared_ptr<IDvrCallback> dvrPlaybackCallback =
+                ::ndk::SharedRefBase::make<DvrPlaybackCallback>();
+
+        ::ndk::ScopedAStatus status =
+                openDvr(DvrType::PLAYBACK, IPTV_BUFFER_SIZE, dvrPlaybackCallback, &iptvDvr);
+        if (status.isOk()) {
+            ALOGI("DVR instance created");
+        }
+
+        // get plugin interface from frontend
+        dtv_plugin* interface = mFrontend->getIptvPluginInterface();
+        if (interface == nullptr) {
+            ALOGE("[Demux] getIptvPluginInterface(): plugin interface is null");
+            return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                    static_cast<int32_t>(Result::INVALID_STATE));
+        }
+        ALOGI("[Demux] getIptvPluginInterface(): plugin interface is not null");
+
+        // get streamer object from Frontend instance
+        dtv_streamer* streamer = mFrontend->getIptvPluginStreamer();
+        if (streamer == nullptr) {
+            ALOGE("[Demux] getIptvPluginStreamer(): streamer is null");
+            return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                    static_cast<int32_t>(Result::INVALID_STATE));
+        }
+        ALOGI("[Demux] getIptvPluginStreamer(): streamer is not null");
+
+        // get transport description from frontend
+        string transport_desc = mFrontend->getIptvTransportDescription();
+        ALOGI("[Demux] getIptvTransportDescription(): transport_desc: %s", transport_desc.c_str());
+
+        // call read_stream on the socket to populate the buffer with TS data
+        // while thread is alive, keep reading data
+        int timeout_ms = 20;
+        int buffer_timeout = 10000;  // 10s
+        void* buf = malloc(sizeof(char) * IPTV_BUFFER_SIZE);
+        if (buf == nullptr) ALOGI("malloc buf failed");
+        ALOGI("[   INFO   ] Allocated buffer of size %d", IPTV_BUFFER_SIZE);
+        ALOGI("Getting FMQ from DVR instance to write socket data");
+        mDemuxIptvReadThreadRunning = true;
+        mDemuxIptvReadThread = std::thread(&Demux::readIptvThreadLoop, this, interface, streamer,
+                                           buf, IPTV_BUFFER_SIZE, timeout_ms, buffer_timeout);
+        if (mDemuxIptvReadThread.joinable()) {
+            mDemuxIptvReadThread.join();
+        }
+        free(buf);
+    }
     return ::ndk::ScopedAStatus::ok();
 }
 
@@ -193,61 +360,6 @@
     return ::ndk::ScopedAStatus::ok();
 }
 
-::ndk::ScopedAStatus Demux::openDvr(DvrType in_type, int32_t in_bufferSize,
-                                    const std::shared_ptr<IDvrCallback>& in_cb,
-                                    std::shared_ptr<IDvr>* _aidl_return) {
-    ALOGV("%s", __FUNCTION__);
-
-    if (in_cb == nullptr) {
-        ALOGW("[Demux] DVR callback can't be null");
-        *_aidl_return = nullptr;
-        return ::ndk::ScopedAStatus::fromServiceSpecificError(
-                static_cast<int32_t>(Result::INVALID_ARGUMENT));
-    }
-
-    set<int64_t>::iterator it;
-    switch (in_type) {
-        case DvrType::PLAYBACK:
-            mDvrPlayback = ndk::SharedRefBase::make<Dvr>(in_type, in_bufferSize, in_cb,
-                                                         this->ref<Demux>());
-            if (!mDvrPlayback->createDvrMQ()) {
-                mDvrPlayback = nullptr;
-                *_aidl_return = mDvrPlayback;
-                return ::ndk::ScopedAStatus::fromServiceSpecificError(
-                        static_cast<int32_t>(Result::UNKNOWN_ERROR));
-            }
-
-            for (it = mPlaybackFilterIds.begin(); it != mPlaybackFilterIds.end(); it++) {
-                if (!mDvrPlayback->addPlaybackFilter(*it, mFilters[*it])) {
-                    ALOGE("[Demux] Can't get filter info for DVR playback");
-                    mDvrPlayback = nullptr;
-                    *_aidl_return = mDvrPlayback;
-                    return ::ndk::ScopedAStatus::fromServiceSpecificError(
-                            static_cast<int32_t>(Result::UNKNOWN_ERROR));
-                }
-            }
-
-            *_aidl_return = mDvrPlayback;
-            return ::ndk::ScopedAStatus::ok();
-        case DvrType::RECORD:
-            mDvrRecord = ndk::SharedRefBase::make<Dvr>(in_type, in_bufferSize, in_cb,
-                                                       this->ref<Demux>());
-            if (!mDvrRecord->createDvrMQ()) {
-                mDvrRecord = nullptr;
-                *_aidl_return = mDvrRecord;
-                return ::ndk::ScopedAStatus::fromServiceSpecificError(
-                        static_cast<int32_t>(Result::UNKNOWN_ERROR));
-            }
-
-            *_aidl_return = mDvrRecord;
-            return ::ndk::ScopedAStatus::ok();
-        default:
-            *_aidl_return = nullptr;
-            return ::ndk::ScopedAStatus::fromServiceSpecificError(
-                    static_cast<int32_t>(Result::INVALID_ARGUMENT));
-    }
-}
-
 ::ndk::ScopedAStatus Demux::connectCiCam(int32_t in_ciCamId) {
     ALOGV("%s", __FUNCTION__);
 
diff --git a/tv/tuner/aidl/default/Demux.h b/tv/tuner/aidl/default/Demux.h
index 7d7aee4..a23063f 100644
--- a/tv/tuner/aidl/default/Demux.h
+++ b/tv/tuner/aidl/default/Demux.h
@@ -17,6 +17,7 @@
 #pragma once
 
 #include <aidl/android/hardware/tv/tuner/BnDemux.h>
+#include <aidl/android/hardware/tv/tuner/BnDvrCallback.h>
 
 #include <fmq/AidlMessageQueue.h>
 #include <math.h>
@@ -28,7 +29,9 @@
 #include "Filter.h"
 #include "Frontend.h"
 #include "TimeFilter.h"
+#include "Timer.h"
 #include "Tuner.h"
+#include "dtv_plugin.h"
 
 using namespace std;
 
@@ -44,6 +47,8 @@
 using ::android::hardware::EventFlag;
 
 using FilterMQ = AidlMessageQueue<int8_t, SynchronizedReadWrite>;
+using AidlMQ = AidlMessageQueue<int8_t, SynchronizedReadWrite>;
+using AidlMQDesc = MQDescriptor<int8_t, SynchronizedReadWrite>;
 
 class Dvr;
 class Filter;
@@ -51,6 +56,19 @@
 class TimeFilter;
 class Tuner;
 
+class DvrPlaybackCallback : public BnDvrCallback {
+  public:
+    virtual ::ndk::ScopedAStatus onPlaybackStatus(PlaybackStatus status) override {
+        ALOGD("demux.h: playback status %d", status);
+        return ndk::ScopedAStatus::ok();
+    }
+
+    virtual ::ndk::ScopedAStatus onRecordStatus(RecordStatus status) override {
+        ALOGD("Record Status %hhd", status);
+        return ndk::ScopedAStatus::ok();
+    }
+};
+
 class Demux : public BnDemux {
   public:
     Demux(int32_t demuxId, uint32_t filterTypes);
@@ -85,6 +103,8 @@
     void setIsRecording(bool isRecording);
     bool isRecording();
     void startFrontendInputLoop();
+    void readIptvThreadLoop(dtv_plugin* interface, dtv_streamer* streamer, void* buf, size_t size,
+                            int timeout_ms, int buffer_timeout);
 
     /**
      * A dispatcher to read and dispatch input data to all the started filters.
@@ -167,11 +187,16 @@
 
     // Thread handlers
     std::thread mFrontendInputThread;
+    std::thread mDemuxIptvReadThread;
+
+    // track whether the DVR FMQ for IPTV Playback is full
+    bool mIsIptvDvrFMQFull = false;
 
     /**
      * If a specific filter's writing loop is still running
      */
     std::atomic<bool> mFrontendInputThreadRunning;
+    std::atomic<bool> mDemuxIptvReadThreadRunning;
     std::atomic<bool> mKeepFetchingDataFromFrontend;
 
     /**
diff --git a/tv/tuner/aidl/default/Dvr.cpp b/tv/tuner/aidl/default/Dvr.cpp
index c046ae3..393200c 100644
--- a/tv/tuner/aidl/default/Dvr.cpp
+++ b/tv/tuner/aidl/default/Dvr.cpp
@@ -236,6 +236,25 @@
     ALOGD("[Dvr] playback thread ended.");
 }
 
+void Dvr::maySendIptvPlaybackStatusCallback() {
+    lock_guard<mutex> lock(mPlaybackStatusLock);
+    int availableToRead = mDvrMQ->availableToRead();
+    int availableToWrite = mDvrMQ->availableToWrite();
+
+    PlaybackStatus newStatus = checkPlaybackStatusChange(availableToWrite, availableToRead,
+                                                         IPTV_PLAYBACK_STATUS_THRESHOLD_HIGH,
+                                                         IPTV_PLAYBACK_STATUS_THRESHOLD_LOW);
+    if (mPlaybackStatus != newStatus) {
+        map<int64_t, std::shared_ptr<Filter>>::iterator it;
+        for (it = mFilters.begin(); it != mFilters.end(); it++) {
+            std::shared_ptr<Filter> currentFilter = it->second;
+            currentFilter->setIptvDvrPlaybackStatus(newStatus);
+        }
+        mCallback->onPlaybackStatus(newStatus);
+        mPlaybackStatus = newStatus;
+    }
+}
+
 void Dvr::maySendPlaybackStatusCallback() {
     lock_guard<mutex> lock(mPlaybackStatusLock);
     int availableToRead = mDvrMQ->availableToRead();
@@ -379,7 +398,7 @@
 
     // Read es raw data from the FMQ per meta data built previously
     vector<int8_t> frameData;
-    map<int64_t, std::shared_ptr<IFilter>>::iterator it;
+    map<int64_t, std::shared_ptr<Filter>>::iterator it;
     int pid = 0;
     for (int i = 0; i < totalFrames; i++) {
         frameData.resize(esMeta[i].len);
@@ -411,7 +430,7 @@
 }
 
 void Dvr::startTpidFilter(vector<int8_t> data) {
-    map<int64_t, std::shared_ptr<IFilter>>::iterator it;
+    map<int64_t, std::shared_ptr<Filter>>::iterator it;
     for (it = mFilters.begin(); it != mFilters.end(); it++) {
         uint16_t pid = ((data[1] & 0x1f) << 8) | ((data[2] & 0xff));
         if (DEBUG_DVR) {
@@ -432,7 +451,7 @@
         }
     }
 
-    map<int64_t, std::shared_ptr<IFilter>>::iterator it;
+    map<int64_t, std::shared_ptr<Filter>>::iterator it;
     // Handle the output data per filter type
     for (it = mFilters.begin(); it != mFilters.end(); it++) {
         if (!mDemux->startFilterHandler(it->first).isOk()) {
@@ -443,6 +462,24 @@
     return true;
 }
 
+int Dvr::writePlaybackFMQ(void* buf, size_t size) {
+    lock_guard<mutex> lock(mWriteLock);
+    ALOGI("Playback status: %d", mPlaybackStatus);
+    if (mPlaybackStatus == PlaybackStatus::SPACE_FULL) {
+        ALOGW("[Dvr] stops writing and wait for the client side flushing.");
+        return DVR_WRITE_FAILURE_REASON_FMQ_FULL;
+    }
+    ALOGI("availableToWrite before: %zu", mDvrMQ->availableToWrite());
+    if (mDvrMQ->write((int8_t*)buf, size)) {
+        mDvrEventFlag->wake(static_cast<uint32_t>(DemuxQueueNotifyBits::DATA_READY));
+        ALOGI("availableToWrite: %zu", mDvrMQ->availableToWrite());
+        maySendIptvPlaybackStatusCallback();
+        return DVR_WRITE_SUCCESS;
+    }
+    maySendIptvPlaybackStatusCallback();
+    return DVR_WRITE_FAILURE_REASON_UNKNOWN;
+}
+
 bool Dvr::writeRecordFMQ(const vector<int8_t>& data) {
     lock_guard<mutex> lock(mWriteLock);
     if (mRecordStatus == RecordStatus::OVERFLOW) {
@@ -486,7 +523,7 @@
     return mRecordStatus;
 }
 
-bool Dvr::addPlaybackFilter(int64_t filterId, std::shared_ptr<IFilter> filter) {
+bool Dvr::addPlaybackFilter(int64_t filterId, std::shared_ptr<Filter> filter) {
     mFilters[filterId] = filter;
     return true;
 }
diff --git a/tv/tuner/aidl/default/Dvr.h b/tv/tuner/aidl/default/Dvr.h
index 293c533..07f95ad 100644
--- a/tv/tuner/aidl/default/Dvr.h
+++ b/tv/tuner/aidl/default/Dvr.h
@@ -43,6 +43,19 @@
 
 using DvrMQ = AidlMessageQueue<int8_t, SynchronizedReadWrite>;
 
+const int DVR_WRITE_SUCCESS = 0;
+const int DVR_WRITE_FAILURE_REASON_FMQ_FULL = 1;
+const int DVR_WRITE_FAILURE_REASON_UNKNOWN = 2;
+
+const int TS_SIZE = 188;
+const int IPTV_BUFFER_SIZE = TS_SIZE * 7 * 8;  // defined in service_streamer_udp in cbs v3 project
+
+// Thresholds are defined to indicate how full the buffers are.
+const double HIGH_THRESHOLD_PERCENT = 0.90;
+const double LOW_THRESHOLD_PERCENT = 0.15;
+const int IPTV_PLAYBACK_STATUS_THRESHOLD_HIGH = IPTV_BUFFER_SIZE * HIGH_THRESHOLD_PERCENT;
+const int IPTV_PLAYBACK_STATUS_THRESHOLD_LOW = IPTV_BUFFER_SIZE * LOW_THRESHOLD_PERCENT;
+
 struct MediaEsMetaData {
     bool isAudio;
     int startIndex;
@@ -80,8 +93,9 @@
      * Return false is any of the above processes fails.
      */
     bool createDvrMQ();
+    int writePlaybackFMQ(void* buf, size_t size);
     bool writeRecordFMQ(const std::vector<int8_t>& data);
-    bool addPlaybackFilter(int64_t filterId, std::shared_ptr<IFilter> filter);
+    bool addPlaybackFilter(int64_t filterId, std::shared_ptr<Filter> filter);
     bool removePlaybackFilter(int64_t filterId);
     bool readPlaybackFMQ(bool isVirtualFrontend, bool isRecording);
     bool processEsDataOnPlayback(bool isVirtualFrontend, bool isRecording);
@@ -96,12 +110,13 @@
     DvrType mType;
     uint32_t mBufferSize;
     std::shared_ptr<IDvrCallback> mCallback;
-    std::map<int64_t, std::shared_ptr<IFilter>> mFilters;
+    std::map<int64_t, std::shared_ptr<Filter>> mFilters;
 
     void deleteEventFlag();
     bool readDataFromMQ();
     void getMetaDataValue(int& index, int8_t* dataOutputBuffer, int& value);
     void maySendPlaybackStatusCallback();
+    void maySendIptvPlaybackStatusCallback();
     void maySendRecordStatusCallback();
     PlaybackStatus checkPlaybackStatusChange(uint32_t availableToWrite, uint32_t availableToRead,
                                              int64_t highThreshold, int64_t lowThreshold);
diff --git a/tv/tuner/aidl/default/Filter.cpp b/tv/tuner/aidl/default/Filter.cpp
index ba9602e..7b62c3e 100644
--- a/tv/tuner/aidl/default/Filter.cpp
+++ b/tv/tuner/aidl/default/Filter.cpp
@@ -565,6 +565,8 @@
 
     ALOGD("[Filter] filter %" PRIu64 " threadLoop start.", mFilterId);
 
+    ALOGI("IPTV DVR Playback status on Filter: %d", mIptvDvrPlaybackStatus);
+
     // For the first time of filter output, implementation needs to send the filter
     // Event Callback without waiting for the DATA_CONSUMED to init the process.
     while (mFilterThreadRunning) {
diff --git a/tv/tuner/aidl/default/Filter.h b/tv/tuner/aidl/default/Filter.h
index 0985296..1cf6b3a 100644
--- a/tv/tuner/aidl/default/Filter.h
+++ b/tv/tuner/aidl/default/Filter.h
@@ -147,6 +147,7 @@
     bool isMediaFilter() { return mIsMediaFilter; };
     bool isPcrFilter() { return mIsPcrFilter; };
     bool isRecordFilter() { return mIsRecordFilter; };
+    void setIptvDvrPlaybackStatus(PlaybackStatus newStatus) { mIptvDvrPlaybackStatus = newStatus; };
 
   private:
     // Demux service
@@ -286,6 +287,8 @@
     int mStartId = 0;
     uint8_t mScramblingStatusMonitored = 0;
     uint8_t mIpCidMonitored = 0;
+
+    PlaybackStatus mIptvDvrPlaybackStatus;
 };
 
 }  // namespace tuner
diff --git a/tv/tuner/aidl/default/Frontend.cpp b/tv/tuner/aidl/default/Frontend.cpp
index cd072bf..6bdbac5 100644
--- a/tv/tuner/aidl/default/Frontend.cpp
+++ b/tv/tuner/aidl/default/Frontend.cpp
@@ -213,20 +213,82 @@
     return ::ndk::ScopedAStatus::ok();
 }
 
-::ndk::ScopedAStatus Frontend::tune(const FrontendSettings& /* in_settings */) {
-    ALOGV("%s", __FUNCTION__);
+void Frontend::readTuneByte(dtv_streamer* streamer, void* buf, size_t buf_size, int timeout_ms) {
+    ssize_t bytes_read = mIptvPluginInterface->read_stream(streamer, buf, buf_size, timeout_ms);
+    if (bytes_read == 0) {
+        ALOGI("[   ERROR   ] Tune byte couldn't be read.");
+        return;
+    }
+    mCallback->onEvent(FrontendEventType::LOCKED);
+    mIsLocked = true;
+}
+
+::ndk::ScopedAStatus Frontend::tune(const FrontendSettings& in_settings) {
     if (mCallback == nullptr) {
-        ALOGW("[   WARN   ] Frontend callback is not set when tune");
+        ALOGW("[   WARN   ] Frontend callback is not set for tunin0g");
         return ::ndk::ScopedAStatus::fromServiceSpecificError(
                 static_cast<int32_t>(Result::INVALID_STATE));
     }
 
     if (mType != FrontendType::IPTV) {
         mTuner->frontendStartTune(mId);
-    }
+        mCallback->onEvent(FrontendEventType::LOCKED);
+        mIsLocked = true;
+    } else {
+        // This is a reference implementation for IPTV. It uses an additional socket buffer.
+        // Vendors can use hardware memory directly to make the implementation more performant.
+        ALOGI("[   INFO   ] Frontend type is set to IPTV, tag = %d id=%d", in_settings.getTag(),
+              mId);
 
-    mCallback->onEvent(FrontendEventType::LOCKED);
-    mIsLocked = true;
+        // load udp plugin for reading TS data
+        const char* path = "/vendor/lib/iptv_udp_plugin.so";
+        DtvPlugin* plugin = new DtvPlugin(path);
+        if (!plugin) {
+            ALOGE("Failed to create DtvPlugin, plugin_path is invalid");
+            return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                    static_cast<int32_t>(Result::INVALID_ARGUMENT));
+        }
+        bool plugin_loaded = plugin->load();
+        if (!plugin_loaded) {
+            ALOGE("Failed to load plugin");
+            return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                    static_cast<int32_t>(Result::INVALID_ARGUMENT));
+        }
+        mIptvPluginInterface = plugin->interface();
+
+        // validate content_url format
+        std::string content_url = in_settings.get<FrontendSettings::Tag::iptv>()->contentUrl;
+        std::string transport_desc = "{ \"uri\": \"" + content_url + "\"}";
+        ALOGI("[   INFO   ] transport_desc: %s", transport_desc.c_str());
+        bool is_transport_desc_valid = plugin->validate(transport_desc.c_str());
+        if (!is_transport_desc_valid) {  // not of format protocol://ip:port
+            ALOGE("[   INFO   ] transport_desc is not valid");
+            return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                    static_cast<int32_t>(Result::INVALID_ARGUMENT));
+        }
+        mIptvTransportDescription = transport_desc;
+
+        // create a streamer and open it for reading data
+        dtv_streamer* streamer = mIptvPluginInterface->create_streamer();
+        mIptvPluginStreamer = streamer;
+        int open_fd = mIptvPluginInterface->open_stream(streamer, transport_desc.c_str());
+        if (open_fd < 0) {
+            ALOGE("[   INFO   ] could not open stream");
+            return ::ndk::ScopedAStatus::fromServiceSpecificError(
+                    static_cast<int32_t>(Result::INVALID_ARGUMENT));
+        }
+        ALOGI("[   INFO   ] open_stream successful, open_fd=%d", open_fd);
+
+        size_t buf_size = 1;
+        int timeout_ms = 2000;
+        void* buf = malloc(sizeof(char) * buf_size);
+        if (buf == nullptr) ALOGI("malloc buf failed [TUNE]");
+        ALOGI("[   INFO   ] [Tune] Allocated buffer of size %zu", buf_size);
+        mIptvFrontendTuneThread =
+                std::thread(&Frontend::readTuneByte, this, streamer, buf, buf_size, timeout_ms);
+        if (mIptvFrontendTuneThread.joinable()) mIptvFrontendTuneThread.join();
+        free(buf);
+    }
 
     return ::ndk::ScopedAStatus::ok();
 }
@@ -1002,6 +1064,18 @@
     return mId;
 }
 
+dtv_plugin* Frontend::getIptvPluginInterface() {
+    return mIptvPluginInterface;
+}
+
+string Frontend::getIptvTransportDescription() {
+    return mIptvTransportDescription;
+}
+
+dtv_streamer* Frontend::getIptvPluginStreamer() {
+    return mIptvPluginStreamer;
+}
+
 bool Frontend::supportsSatellite() {
     return mType == FrontendType::DVBS || mType == FrontendType::ISDBS ||
            mType == FrontendType::ISDBS3;
diff --git a/tv/tuner/aidl/default/Frontend.h b/tv/tuner/aidl/default/Frontend.h
index 85bd636..17a1aee 100644
--- a/tv/tuner/aidl/default/Frontend.h
+++ b/tv/tuner/aidl/default/Frontend.h
@@ -21,6 +21,7 @@
 #include <iostream>
 #include <thread>
 #include "Tuner.h"
+#include "dtv_plugin.h"
 
 using namespace std;
 
@@ -60,6 +61,10 @@
     FrontendType getFrontendType();
     int32_t getFrontendId();
     string getSourceFile();
+    dtv_plugin* getIptvPluginInterface();
+    string getIptvTransportDescription();
+    dtv_streamer* getIptvPluginStreamer();
+    void readTuneByte(dtv_streamer* streamer, void* buf, size_t size, int timeout_ms);
     bool isLocked();
     void getFrontendInfo(FrontendInfo* _aidl_return);
     void setTunerService(std::shared_ptr<Tuner> tuner);
@@ -81,6 +86,10 @@
     std::ifstream mFrontendData;
     FrontendCapabilities mFrontendCaps;
     vector<FrontendStatusType> mFrontendStatusCaps;
+    dtv_plugin* mIptvPluginInterface;
+    string mIptvTransportDescription;
+    dtv_streamer* mIptvPluginStreamer;
+    std::thread mIptvFrontendTuneThread;
 };
 
 }  // namespace tuner
diff --git a/tv/tuner/aidl/default/Timer.h b/tv/tuner/aidl/default/Timer.h
new file mode 100644
index 0000000..c6327cb
--- /dev/null
+++ b/tv/tuner/aidl/default/Timer.h
@@ -0,0 +1,17 @@
+#include <chrono>
+using namespace std::chrono;
+class Timer {
+  public:
+    Timer() { start_time = steady_clock::now(); }
+
+    ~Timer() { stop_time = steady_clock::now(); }
+
+    double get_elapsed_time_ms() {
+        auto current_time = std::chrono::steady_clock::now();
+        return duration_cast<milliseconds>(current_time - start_time).count();
+    }
+
+  private:
+    time_point<steady_clock> start_time;
+    time_point<steady_clock> stop_time;
+};
\ No newline at end of file
diff --git a/tv/tuner/aidl/default/dtv_plugin.cpp b/tv/tuner/aidl/default/dtv_plugin.cpp
new file mode 100644
index 0000000..4e73ee5
--- /dev/null
+++ b/tv/tuner/aidl/default/dtv_plugin.cpp
@@ -0,0 +1,130 @@
+#include "dtv_plugin.h"
+#include <dlfcn.h>
+#include <libgen.h>
+#include <utils/Log.h>
+
+DtvPlugin::DtvPlugin(const char* plugin_path) {
+    path_ = plugin_path;
+    basename_ = basename(path_);
+    module_ = NULL;
+    interface_ = NULL;
+    loaded_ = false;
+}
+
+DtvPlugin::~DtvPlugin() {
+    if (module_ != NULL) {
+        if (dlclose(module_)) ALOGE("DtvPlugin: Failed to close plugin '%s'", basename_);
+    }
+}
+
+bool DtvPlugin::load() {
+    ALOGI("Loading plugin '%s' from path '%s'", basename_, path_);
+
+    module_ = dlopen(path_, RTLD_LAZY);
+    if (module_ == NULL) {
+        ALOGE("DtvPlugin::Load::Failed to load plugin '%s'", basename_);
+        ALOGE("dlopen error: %s", dlerror());
+        return false;
+    }
+
+    interface_ = (dtv_plugin*)dlsym(module_, "plugin_entry");
+
+    if (interface_ == NULL) {
+        ALOGE("plugin_entry is NULL.");
+        goto error;
+    }
+
+    if (!interface_->get_transport_types || !interface_->get_streamer_count ||
+        !interface_->validate || !interface_->create_streamer || !interface_->destroy_streamer ||
+        !interface_->open_stream || !interface_->close_stream || !interface_->read_stream) {
+        ALOGW("Plugin: missing one or more callbacks");
+        goto error;
+    }
+
+    loaded_ = true;
+
+    return true;
+
+error:
+    if (dlclose(module_)) ALOGE("Failed to close plugin '%s'", basename_);
+
+    return false;
+}
+
+int DtvPlugin::getStreamerCount() {
+    if (!loaded_) {
+        ALOGE("DtvPlugin::GetStreamerCount: Plugin '%s' not loaded!", basename_);
+        return 0;
+    }
+
+    return interface_->get_streamer_count();
+}
+
+bool DtvPlugin::isTransportTypeSupported(const char* transport_type) {
+    const char** transport;
+
+    if (!loaded_) {
+        ALOGE("Plugin '%s' not loaded!", basename_);
+        return false;
+    }
+
+    transport = interface_->get_transport_types();
+    if (transport == NULL) return false;
+
+    while (*transport) {
+        if (strcmp(transport_type, *transport) == 0) return true;
+        transport++;
+    }
+
+    return false;
+}
+
+bool DtvPlugin::validate(const char* transport_desc) {
+    if (!loaded_) {
+        ALOGE("Plugin '%s' is not loaded!", basename_);
+        return false;
+    }
+
+    return interface_->validate(transport_desc);
+}
+
+bool DtvPlugin::getProperty(const char* key, void* value, int* size) {
+    if (!loaded_) {
+        ALOGE("Plugin '%s' is not loaded!", basename_);
+        return false;
+    }
+
+    if (!interface_->get_property) return false;
+
+    *size = interface_->get_property(NULL, key, value, *size);
+
+    return *size < 0 ? false : true;
+}
+
+bool DtvPlugin::setProperty(const char* key, const void* value, int size) {
+    int ret;
+
+    if (!loaded_) {
+        ALOGE("Plugin '%s': not loaded!", basename_);
+        return false;
+    }
+
+    if (!interface_->set_property) return false;
+
+    ret = interface_->set_property(NULL, key, value, size);
+
+    return ret < 0 ? false : true;
+}
+
+struct dtv_plugin* DtvPlugin::interface() {
+    if (!loaded_) {
+        ALOGE("Plugin '%s' is not loaded!", basename_);
+        return NULL;
+    }
+
+    return interface_;
+}
+
+const char* DtvPlugin::pluginBasename() {
+    return basename_;
+}
diff --git a/tv/tuner/aidl/default/dtv_plugin.h b/tv/tuner/aidl/default/dtv_plugin.h
new file mode 100644
index 0000000..0ee5489
--- /dev/null
+++ b/tv/tuner/aidl/default/dtv_plugin.h
@@ -0,0 +1,31 @@
+#ifndef LIVE_DTV_PLUGIN_H_
+#define LIVE_DTV_PLUGIN_H_
+
+#include <fstream>
+#include "dtv_plugin_api.h"
+
+class DtvPlugin {
+  public:
+    DtvPlugin(const char* plugin_path);
+    ~DtvPlugin();
+
+    bool load();
+    int getStreamerCount();
+    bool validate(const char* transport_desc);
+    bool isTransportTypeSupported(const char* transport_type);
+    //    /* plugin-wide properties */
+    bool getProperty(const char* key, void* value, int* size);
+    bool setProperty(const char* key, const void* value, int size);
+
+    struct dtv_plugin* interface();
+    const char* pluginBasename();
+
+  protected:
+    const char* path_;
+    char* basename_;
+    void* module_;
+    struct dtv_plugin* interface_;
+    bool loaded_;
+};
+
+#endif  // LIVE_DTV_PLUGIN_H_
diff --git a/tv/tuner/aidl/default/dtv_plugin_api.h b/tv/tuner/aidl/default/dtv_plugin_api.h
new file mode 100644
index 0000000..8fe7c1d
--- /dev/null
+++ b/tv/tuner/aidl/default/dtv_plugin_api.h
@@ -0,0 +1,137 @@
+#ifndef LIVE_DTV_PLUGIN_API_H_
+#define LIVE_DTV_PLUGIN_API_H_
+
+#include <stdint.h>
+
+struct dtv_streamer;
+
+struct dtv_plugin {
+    uint32_t version;
+
+    /**
+     * get_transport_types() - Retrieve a list of supported transport types.
+     *
+     * Return: A NULL-terminated list of supported transport types.
+     */
+    const char** (*get_transport_types)(void);
+
+    /**
+     * get_streamer_count() - Get number of streamers that can be created.
+     *
+     * Return: The number of streamers that can be created.
+     */
+    int (*get_streamer_count)(void);
+
+    /**
+     * validate() - Check if transport description is valid.
+     * @transport_desc: NULL-terminated transport description in json format.
+     *
+     * Return: 1 if valid, 0 otherwise.
+     */
+    int (*validate)(const char* transport_desc);
+
+    /**
+     * create_streamer() - Create a streamer object.
+     *
+     * Return: A pointer to a new streamer object.
+     */
+    struct dtv_streamer* (*create_streamer)(void);
+
+    /**
+     * destroy_streamer() - Free a streamer object and all associated resources.
+     * @st: Pointer to a streamer object
+     */
+    void (*destroy_streamer)(struct dtv_streamer* streamer);
+
+    /**
+     * set_property() - Set a key/value pair property.
+     * @streamer: Pointer to a streamer object (may be NULL for plugin-wide properties).
+     * @key: NULL-terminated property name.
+     * @value: Property value.
+     * @size: Property value size.
+     *
+     * Return: 0 if success, -1 otherwise.
+     */
+    int (*set_property)(struct dtv_streamer* streamer, const char* key, const void* value,
+                        size_t size);
+
+    /**
+     * get_property() - Get a property's value.
+     * @streamer: Pointer to a streamer (may be NULL for plugin-wide properties).
+     * @key: NULL-terminated property name.
+     * @value: Property value.
+     * @size: Property value size.
+     *
+     * Return: >= 0 if success, -1 otherwise.
+     *
+     * If size is 0, get_property will return the size needed to hold the value.
+     */
+    int (*get_property)(struct dtv_streamer* streamer, const char* key, void* value, size_t size);
+
+    /**
+     * add_pid() - Add a TS filter on a given pid.
+     * @streamer: The streamer that outputs the TS.
+     * @pid: The pid to add to the TS output.
+     *
+     * Return: 0 if success, -1 otherwise.
+     *
+     * This function is optional but can be useful if a hardware remux is
+     * available.
+     */
+    int (*add_pid)(struct dtv_streamer* streamer, int pid);
+
+    /**
+     * remove_pid() - Remove a TS filter on a given pid.
+     * @streamer: The streamer that outputs the TS.
+     * @pid: The pid to remove from the TS output.
+     *
+     * Return: 0 if success, -1 otherwise.
+     *
+     * This function is optional.
+     */
+    int (*remove_pid)(struct dtv_streamer* streamer, int pid);
+
+    /**
+     * open_stream() - Open a stream from a transport description.
+     * @streamer: The streamer which will handle the stream.
+     * @transport_desc: NULL-terminated transport description in json format.
+     *
+     * The streamer will allocate the resources and make the appropriate
+     * connections to handle this transport.
+     * This function returns a file descriptor that can be polled for events.
+     *
+     * Return: A file descriptor if success, -1 otherwise.
+     */
+    int (*open_stream)(struct dtv_streamer* streamer, const char* transport_desc);
+
+    /**
+     * close_stream() - Release an open stream.
+     * @streamer: The streamer from which the stream should be released.
+     */
+    void (*close_stream)(struct dtv_streamer* streamer);
+
+    /**
+     * read_stream() - Read stream data.
+     * @streamer: The streamer to read from.
+     * @buf: The destination buffer.
+     * @count: The number of bytes to read.
+     * @timeout_ms: Timeout in ms.
+     *
+     * Return: The number of bytes read, -1 if error.
+     */
+    ssize_t (*read_stream)(struct dtv_streamer* streamer, void* buf, size_t count, int timeout_ms);
+};
+
+struct dtv_plugin_event {
+    int id;
+    char data[0];
+};
+
+enum {
+    DTV_PLUGIN_EVENT_SIGNAL_LOST = 1,
+    DTV_PLUGIN_EVENT_SIGNAL_READY,
+};
+
+#define PROPERTY_STATISTICS "statistics"
+
+#endif  // LIVE_DTV_PLUGIN_API_H_
diff --git a/usb/aidl/Android.bp b/usb/aidl/Android.bp
index b82f6d5..b61576d 100644
--- a/usb/aidl/Android.bp
+++ b/usb/aidl/Android.bp
@@ -45,6 +45,6 @@
         },
 
     ],
-    frozen: true,
+    frozen: false,
 
 }
diff --git a/usb/aidl/aidl_api/android.hardware.usb/current/android/hardware/usb/ComplianceWarning.aidl b/usb/aidl/aidl_api/android.hardware.usb/current/android/hardware/usb/ComplianceWarning.aidl
index 8b67070..c7c9103 100644
--- a/usb/aidl/aidl_api/android.hardware.usb/current/android/hardware/usb/ComplianceWarning.aidl
+++ b/usb/aidl/aidl_api/android.hardware.usb/current/android/hardware/usb/ComplianceWarning.aidl
@@ -38,4 +38,9 @@
   DEBUG_ACCESSORY = 2,
   BC_1_2 = 3,
   MISSING_RP = 4,
+  INPUT_POWER_LIMITED = 5,
+  MISSING_DATA_LINES = 6,
+  ENUMERATION_FAIL = 7,
+  FLAKY_CONNECTION = 8,
+  UNRELIABLE_IO = 9,
 }
diff --git a/usb/aidl/android/hardware/usb/ComplianceWarning.aidl b/usb/aidl/android/hardware/usb/ComplianceWarning.aidl
index 4c18a31..bf79399 100644
--- a/usb/aidl/android/hardware/usb/ComplianceWarning.aidl
+++ b/usb/aidl/android/hardware/usb/ComplianceWarning.aidl
@@ -56,4 +56,29 @@
      * Type-C Cable and Connector Specification.
      */
     MISSING_RP = 4,
+    /**
+     * Used to indicate the charging setups on the USB ports are unable to
+     * deliver negotiated power.
+     */
+    INPUT_POWER_LIMITED = 5,
+    /**
+     * Used to indicate the cable/connector on the USB ports are missing
+     * the required wires on the data pins to make data transfer.
+     */
+    MISSING_DATA_LINES = 6,
+    /**
+     * Used to indicate enumeration failures on the USB ports, potentially due to
+     * signal integrity issues or other causes.
+     */
+    ENUMERATION_FAIL = 7,
+    /**
+     * Used to indicate unexpected data disconnection on the USB ports,
+     * potentially due to signal integrity issues or other causes.
+     */
+    FLAKY_CONNECTION = 8,
+    /**
+     * Used to indicate unreliable or slow data transfer on the USB ports,
+     * potentially due to signal integrity issues or other causes.
+     */
+    UNRELIABLE_IO = 9,
 }
diff --git a/usb/aidl/default/Android.bp b/usb/aidl/default/Android.bp
index 2c6ed07..ee8e479 100644
--- a/usb/aidl/default/Android.bp
+++ b/usb/aidl/default/Android.bp
@@ -34,7 +34,7 @@
         "Usb.cpp",
     ],
     shared_libs: [
-        "android.hardware.usb-V2-ndk",
+        "android.hardware.usb-V3-ndk",
         "libbase",
         "libbinder_ndk",
         "libcutils",
diff --git a/usb/aidl/default/android.hardware.usb-service.example.xml b/usb/aidl/default/android.hardware.usb-service.example.xml
index c3f07f5..7ac2067 100644
--- a/usb/aidl/default/android.hardware.usb-service.example.xml
+++ b/usb/aidl/default/android.hardware.usb-service.example.xml
@@ -1,7 +1,7 @@
 <manifest version="1.0" type="device">
     <hal format="aidl">
         <name>android.hardware.usb</name>
-        <version>2</version>
+        <version>3</version>
         <interface>
             <name>IUsb</name>
             <instance>default</instance>
diff --git a/usb/aidl/vts/Android.bp b/usb/aidl/vts/Android.bp
index d0e0eec..cf9299e 100644
--- a/usb/aidl/vts/Android.bp
+++ b/usb/aidl/vts/Android.bp
@@ -34,7 +34,7 @@
         "libbinder_ndk",
     ],
     static_libs: [
-        "android.hardware.usb-V2-ndk",
+        "android.hardware.usb-V3-ndk",
     ],
     test_suites: [
         "general-tests",
diff --git a/usb/aidl/vts/VtsAidlUsbTargetTest.cpp b/usb/aidl/vts/VtsAidlUsbTargetTest.cpp
index e9aa65b..7b7269d 100644
--- a/usb/aidl/vts/VtsAidlUsbTargetTest.cpp
+++ b/usb/aidl/vts/VtsAidlUsbTargetTest.cpp
@@ -654,11 +654,18 @@
   EXPECT_EQ(2, usb_last_cookie);
   EXPECT_EQ(transactionId, last_transactionId);
 
-  // Current compliance values range from [1, 4]
   if (usb_last_port_status.supportsComplianceWarnings) {
     for (auto warning : usb_last_port_status.complianceWarnings) {
       EXPECT_TRUE((int)warning >= (int)ComplianceWarning::OTHER);
-      EXPECT_TRUE((int)warning <= (int)ComplianceWarning::MISSING_RP);
+      /*
+       * Version 2 compliance values range from [1, 4]
+       * Version 3 compliance values range from [1, 9]
+       */
+      if (usb_version < 3) {
+        EXPECT_TRUE((int)warning <= (int)ComplianceWarning::MISSING_RP);
+      } else {
+        EXPECT_TRUE((int)warning <= (int)ComplianceWarning::UNRELIABLE_IO);
+      }
     }
   }
 
diff --git a/wifi/common/aidl/Android.bp b/wifi/common/aidl/Android.bp
new file mode 100644
index 0000000..1913451
--- /dev/null
+++ b/wifi/common/aidl/Android.bp
@@ -0,0 +1,47 @@
+// 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "hardware_interfaces_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["hardware_interfaces_license"],
+}
+
+aidl_interface {
+    name: "android.hardware.wifi.common",
+    vendor_available: true,
+    srcs: [
+        "android/hardware/wifi/common/*.aidl",
+    ],
+    headers: [
+        "PersistableBundle_aidl",
+    ],
+    stability: "vintf",
+    backend: {
+        java: {
+            sdk_version: "module_current",
+            apex_available: [
+                "//apex_available:platform",
+                "com.android.wifi",
+            ],
+            min_sdk_version: "30",
+        },
+        cpp: {
+            enabled: false,
+        },
+    },
+}
diff --git a/wifi/common/aidl/aidl_api/android.hardware.wifi.common/current/android/hardware/wifi/common/OuiKeyedData.aidl b/wifi/common/aidl/aidl_api/android.hardware.wifi.common/current/android/hardware/wifi/common/OuiKeyedData.aidl
new file mode 100644
index 0000000..640a1f6
--- /dev/null
+++ b/wifi/common/aidl/aidl_api/android.hardware.wifi.common/current/android/hardware/wifi/common/OuiKeyedData.aidl
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.hardware.wifi.common;
+@VintfStability
+parcelable OuiKeyedData {
+  int oui;
+  android.os.PersistableBundle vendorData;
+}
diff --git a/wifi/common/aidl/android/hardware/wifi/common/OuiKeyedData.aidl b/wifi/common/aidl/android/hardware/wifi/common/OuiKeyedData.aidl
new file mode 100644
index 0000000..4e6a99f
--- /dev/null
+++ b/wifi/common/aidl/android/hardware/wifi/common/OuiKeyedData.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.hardware.wifi.common;
+
+import android.os.PersistableBundle;
+
+/**
+ * Data for OUI-based configuration.
+ */
+@VintfStability
+parcelable OuiKeyedData {
+    /**
+     * OUI : 24-bit organizationally unique identifier to identify the vendor/OEM.
+     * See https://standards-oui.ieee.org/ for more information.
+     */
+    int oui;
+    /**
+     * Vendor data. Expected fields should be defined by the vendor.
+     */
+    PersistableBundle vendorData;
+}
diff --git a/wifi/netlinkinterceptor/aidl/default/Android.bp b/wifi/netlinkinterceptor/aidl/default/Android.bp
index 5227e51..c3a0c03 100644
--- a/wifi/netlinkinterceptor/aidl/default/Android.bp
+++ b/wifi/netlinkinterceptor/aidl/default/Android.bp
@@ -25,8 +25,6 @@
 
 cc_binary {
     name: "android.hardware.net.nlinterceptor-service.default",
-    init_rc: ["nlinterceptor-default.rc"],
-    vintf_fragments: ["nlinterceptor-default.xml"],
     vendor: true,
     relative_install_path: "hw",
     defaults: ["nlinterceptor@defaults"],
@@ -45,4 +43,35 @@
         "service.cpp",
         "util.cpp",
     ],
+    installable: false, // installed in APEX
+}
+
+apex {
+    name: "com.android.hardware.net.nlinterceptor",
+    vendor: true,
+    manifest: "apex_manifest.json",
+    file_contexts: "apex_file_contexts",
+    key: "com.android.hardware.key",
+    certificate: ":com.android.hardware.certificate",
+    updatable: false,
+    binaries: [
+        "android.hardware.net.nlinterceptor-service.default",
+    ],
+    prebuilts: [
+        "nlinterceptor.rc",
+        "nlinterceptor.xml",
+    ],
+}
+
+prebuilt_etc {
+    name: "nlinterceptor.rc",
+    src: "nlinterceptor.rc",
+    installable: false,
+}
+
+prebuilt_etc {
+    name: "nlinterceptor.xml",
+    src: "nlinterceptor.xml",
+    sub_dir: "vintf",
+    installable: false,
 }
diff --git a/wifi/netlinkinterceptor/aidl/default/apex_file_contexts b/wifi/netlinkinterceptor/aidl/default/apex_file_contexts
new file mode 100644
index 0000000..6ee544c
--- /dev/null
+++ b/wifi/netlinkinterceptor/aidl/default/apex_file_contexts
@@ -0,0 +1,3 @@
+(/.*)?                                                          u:object_r:vendor_file:s0
+/etc(/.*)?                                                      u:object_r:vendor_configs_file:s0
+/bin/hw/android\.hardware\.net\.nlinterceptor-service\.default	u:object_r:hal_nlinterceptor_default_exec:s0
diff --git a/wifi/netlinkinterceptor/aidl/default/apex_manifest.json b/wifi/netlinkinterceptor/aidl/default/apex_manifest.json
new file mode 100644
index 0000000..4ffeac5
--- /dev/null
+++ b/wifi/netlinkinterceptor/aidl/default/apex_manifest.json
@@ -0,0 +1,4 @@
+{
+    "name": "com.android.hardware.net.nlinterceptor",
+    "version": 1
+}
diff --git a/wifi/netlinkinterceptor/aidl/default/nlinterceptor-default.rc b/wifi/netlinkinterceptor/aidl/default/nlinterceptor-default.rc
deleted file mode 100644
index 353cb27..0000000
--- a/wifi/netlinkinterceptor/aidl/default/nlinterceptor-default.rc
+++ /dev/null
@@ -1,4 +0,0 @@
-service nlinterceptor /vendor/bin/hw/android.hardware.net.nlinterceptor-service.default
-    class hal
-    user root
-    group system inet
diff --git a/wifi/netlinkinterceptor/aidl/default/nlinterceptor.rc b/wifi/netlinkinterceptor/aidl/default/nlinterceptor.rc
new file mode 100644
index 0000000..ec9baa9
--- /dev/null
+++ b/wifi/netlinkinterceptor/aidl/default/nlinterceptor.rc
@@ -0,0 +1,4 @@
+service nlinterceptor /apex/com.android.hardware.net.nlinterceptor/bin/hw/android.hardware.net.nlinterceptor-service.default
+    class hal
+    user root
+    group system inet
diff --git a/wifi/netlinkinterceptor/aidl/default/nlinterceptor-default.xml b/wifi/netlinkinterceptor/aidl/default/nlinterceptor.xml
similarity index 100%
rename from wifi/netlinkinterceptor/aidl/default/nlinterceptor-default.xml
rename to wifi/netlinkinterceptor/aidl/default/nlinterceptor.xml