Merge changes Ic8eaee53,Ide961d91,Ie97f3ce9 am: 99d4458c3b am: d6ccd8631f

Original change: https://android-review.googlesource.com/c/platform/hardware/interfaces/+/2374735

Change-Id: I345f22a9f5cd8df41f200e8c0da3d4c3c228b65f
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/audio/aidl/android/hardware/audio/core/stream-out-async-sm.gv b/audio/aidl/android/hardware/audio/core/stream-out-async-sm.gv
index e25b15a..501dc01 100644
--- a/audio/aidl/android/hardware/audio/core/stream-out-async-sm.gv
+++ b/audio/aidl/android/hardware/audio/core/stream-out-async-sm.gv
@@ -30,6 +30,7 @@
     STANDBY -> PAUSED [label="burst"];                // producer -> active
     IDLE -> STANDBY [label="standby"];                // consumer -> passive
     IDLE -> TRANSFERRING [label="burst"];             // producer -> active
+    IDLE -> ACTIVE [label="burst"];                   // full write
     ACTIVE -> PAUSED [label="pause"];                 // consumer -> passive (not consuming)
     ACTIVE -> DRAINING [label="drain"];               // producer -> passive
     ACTIVE -> TRANSFERRING [label="burst"];           // early unblocking
@@ -45,6 +46,7 @@
     PAUSED -> IDLE [label="flush"];                   // producer -> passive, buffer is cleared
     DRAINING -> IDLE [label="←IStreamCallback.onDrainReady"];
     DRAINING -> TRANSFERRING [label="burst"];         // producer -> active
+    DRAINING -> ACTIVE [label="burst"];               // full write
     DRAINING -> DRAIN_PAUSED [label="pause"];         // consumer -> passive (not consuming)
     DRAIN_PAUSED -> DRAINING [label="start"];         // consumer -> active
     DRAIN_PAUSED -> TRANSFER_PAUSED [label="burst"];  // producer -> active
diff --git a/audio/aidl/android/hardware/audio/core/stream-out-sm.gv b/audio/aidl/android/hardware/audio/core/stream-out-sm.gv
index 6aa5c61..47e7fda 100644
--- a/audio/aidl/android/hardware/audio/core/stream-out-sm.gv
+++ b/audio/aidl/android/hardware/audio/core/stream-out-sm.gv
@@ -31,6 +31,7 @@
     ACTIVE -> ACTIVE [label="burst"];
     ACTIVE -> PAUSED [label="pause"];          // consumer -> passive (not consuming)
     ACTIVE -> DRAINING [label="drain"];        // producer -> passive
+    ACTIVE -> IDLE [label="drain"];            // synchronous drain
     PAUSED -> PAUSED [label="burst"];
     PAUSED -> ACTIVE [label="start"];          // consumer -> active
     PAUSED -> IDLE [label="flush"];            // producer -> passive, buffer is cleared
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index e8b5bfc..13b04cd 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -46,6 +46,7 @@
 using aidl::android::media::audio::common::AudioPortConfig;
 using aidl::android::media::audio::common::AudioPortExt;
 using aidl::android::media::audio::common::AudioProfile;
+using aidl::android::media::audio::common::Boolean;
 using aidl::android::media::audio::common::Int;
 using aidl::android::media::audio::common::PcmType;
 using android::hardware::audio::common::getFrameSizeInBytes;
@@ -138,12 +139,15 @@
         (flags.getTag() == AudioIoFlags::Tag::output &&
          !isBitPositionFlagSet(flags.get<AudioIoFlags::Tag::output>(),
                                AudioOutputFlags::MMAP_NOIRQ))) {
+        StreamContext::DebugParameters params{mDebug.streamTransientStateDelayMs,
+                                              mVendorDebug.forceTransientBurst,
+                                              mVendorDebug.forceSynchronousDrain};
         StreamContext temp(
                 std::make_unique<StreamContext::CommandMQ>(1, true /*configureEventFlagWord*/),
                 std::make_unique<StreamContext::ReplyMQ>(1, true /*configureEventFlagWord*/),
                 portConfigIt->format.value(), portConfigIt->channelMask.value(),
                 std::make_unique<StreamContext::DataMQ>(frameSize * in_bufferSizeFrames),
-                asyncCallback, mDebug.streamTransientStateDelayMs);
+                asyncCallback, params);
         if (temp.isValid()) {
             *out_context = std::move(temp);
         } else {
@@ -976,18 +980,69 @@
     return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
 }
 
+const std::string Module::VendorDebug::kForceTransientBurstName = "aosp.forceTransientBurst";
+const std::string Module::VendorDebug::kForceSynchronousDrainName = "aosp.forceSynchronousDrain";
+
 ndk::ScopedAStatus Module::getVendorParameters(const std::vector<std::string>& in_ids,
                                                std::vector<VendorParameter>* _aidl_return) {
     LOG(DEBUG) << __func__ << ": id count: " << in_ids.size();
-    (void)_aidl_return;
-    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+    bool allParametersKnown = true;
+    for (const auto& id : in_ids) {
+        if (id == VendorDebug::kForceTransientBurstName) {
+            VendorParameter forceTransientBurst{.id = id};
+            forceTransientBurst.ext.setParcelable(Boolean{mVendorDebug.forceTransientBurst});
+            _aidl_return->push_back(std::move(forceTransientBurst));
+        } else if (id == VendorDebug::kForceSynchronousDrainName) {
+            VendorParameter forceSynchronousDrain{.id = id};
+            forceSynchronousDrain.ext.setParcelable(Boolean{mVendorDebug.forceSynchronousDrain});
+            _aidl_return->push_back(std::move(forceSynchronousDrain));
+        } else {
+            allParametersKnown = false;
+            LOG(ERROR) << __func__ << ": unrecognized parameter \"" << id << "\"";
+        }
+    }
+    if (allParametersKnown) return ndk::ScopedAStatus::ok();
+    return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
 }
 
+namespace {
+
+template <typename W>
+bool extractParameter(const VendorParameter& p, decltype(W::value)* v) {
+    std::optional<W> value;
+    binder_status_t result = p.ext.getParcelable(&value);
+    if (result == STATUS_OK && value.has_value()) {
+        *v = value.value().value;
+        return true;
+    }
+    LOG(ERROR) << __func__ << ": failed to read the value of the parameter \"" << p.id
+               << "\": " << result;
+    return false;
+}
+
+}  // namespace
+
 ndk::ScopedAStatus Module::setVendorParameters(const std::vector<VendorParameter>& in_parameters,
                                                bool in_async) {
     LOG(DEBUG) << __func__ << ": parameter count " << in_parameters.size()
                << ", async: " << in_async;
-    return ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION);
+    bool allParametersKnown = true;
+    for (const auto& p : in_parameters) {
+        if (p.id == VendorDebug::kForceTransientBurstName) {
+            if (!extractParameter<Boolean>(p, &mVendorDebug.forceTransientBurst)) {
+                return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+            }
+        } else if (p.id == VendorDebug::kForceSynchronousDrainName) {
+            if (!extractParameter<Boolean>(p, &mVendorDebug.forceSynchronousDrain)) {
+                return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+            }
+        } else {
+            allParametersKnown = false;
+            LOG(ERROR) << __func__ << ": unrecognized parameter \"" << p.id << "\"";
+        }
+    }
+    if (allParametersKnown) return ndk::ScopedAStatus::ok();
+    return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
 }
 
 ndk::ScopedAStatus Module::addDeviceEffect(
diff --git a/audio/aidl/default/Stream.cpp b/audio/aidl/default/Stream.cpp
index a490a2a..0520cba 100644
--- a/audio/aidl/default/Stream.cpp
+++ b/audio/aidl/default/Stream.cpp
@@ -402,7 +402,11 @@
                     usleep(1000);  // Simulate a blocking call into the driver.
                     populateReply(&reply, mIsConnected);
                     // Can switch the state to ERROR if a driver error occurs.
-                    switchToTransientState(StreamDescriptor::State::DRAINING);
+                    if (mState == StreamDescriptor::State::ACTIVE && mForceSynchronousDrain) {
+                        mState = StreamDescriptor::State::IDLE;
+                    } else {
+                        switchToTransientState(StreamDescriptor::State::DRAINING);
+                    }
                 } else if (mState == StreamDescriptor::State::TRANSFER_PAUSED) {
                     mState = StreamDescriptor::State::DRAIN_PAUSED;
                     populateReply(&reply, mIsConnected);
@@ -467,14 +471,19 @@
 
 bool StreamOutWorkerLogic::write(size_t clientSize, StreamDescriptor::Reply* reply) {
     const size_t readByteCount = mDataMQ->availableToRead();
-    // Amount of data that the HAL module is going to actually use.
-    const size_t byteCount = std::min({clientSize, readByteCount, mDataBufferSize});
     bool fatal = false;
     if (bool success = readByteCount > 0 ? mDataMQ->read(&mDataBuffer[0], readByteCount) : true) {
         const bool isConnected = mIsConnected;
         LOG(DEBUG) << __func__ << ": reading of " << readByteCount << " bytes from data MQ"
                    << " succeeded; connected? " << isConnected;
-        // Frames are consumed and counted regardless of connection status.
+        // Amount of data that the HAL module is going to actually use.
+        size_t byteCount = std::min({clientSize, readByteCount, mDataBufferSize});
+        if (byteCount >= mFrameSize && mForceTransientBurst) {
+            // In order to prevent the state machine from going to ACTIVE state,
+            // simulate partial write.
+            byteCount -= mFrameSize;
+        }
+        // Frames are consumed and counted regardless of the connection status.
         reply->fmqByteCount += byteCount;
         mFrameCount += byteCount / mFrameSize;
         populateReply(reply, isConnected);
diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h
index e9f43d8..000a704 100644
--- a/audio/aidl/default/include/core-impl/Module.h
+++ b/audio/aidl/default/include/core-impl/Module.h
@@ -36,6 +36,13 @@
     explicit Module(Type type) : mType(type) {}
 
   private:
+    struct VendorDebug {
+        static const std::string kForceTransientBurstName;
+        static const std::string kForceSynchronousDrainName;
+        bool forceTransientBurst = false;
+        bool forceSynchronousDrain = false;
+    };
+
     ndk::ScopedAStatus setModuleDebug(
             const ::aidl::android::hardware::audio::core::ModuleDebug& in_debug) override;
     ndk::ScopedAStatus getTelephony(std::shared_ptr<ITelephony>* _aidl_return) override;
@@ -128,6 +135,7 @@
     const Type mType;
     std::unique_ptr<internal::Configuration> mConfig;
     ModuleDebug mDebug;
+    VendorDebug mVendorDebug;
     // For the interfaces requiring to return the same instance, we need to hold them
     // via a strong pointer. The binder token is retained for a call to 'setMinSchedulerPolicy'.
     std::shared_ptr<ITelephony> mTelephony;
diff --git a/audio/aidl/default/include/core-impl/Stream.h b/audio/aidl/default/include/core-impl/Stream.h
index 5abd4de..2cf5951 100644
--- a/audio/aidl/default/include/core-impl/Stream.h
+++ b/audio/aidl/default/include/core-impl/Stream.h
@@ -62,12 +62,21 @@
     // Ensure that this value is not used by any of StreamDescriptor.State enums
     static constexpr int32_t STATE_CLOSED = -1;
 
+    struct DebugParameters {
+        // An extra delay for transient states, in ms.
+        int transientStateDelayMs = 0;
+        // Force the "burst" command to move the SM to the TRANSFERRING state.
+        bool forceTransientBurst = false;
+        // Force the "drain" command to be synchronous, going directly to the IDLE state.
+        bool forceSynchronousDrain = false;
+    };
+
     StreamContext() = default;
     StreamContext(std::unique_ptr<CommandMQ> commandMQ, std::unique_ptr<ReplyMQ> replyMQ,
                   const ::aidl::android::media::audio::common::AudioFormatDescription& format,
                   const ::aidl::android::media::audio::common::AudioChannelLayout& channelLayout,
                   std::unique_ptr<DataMQ> dataMQ, std::shared_ptr<IStreamCallback> asyncCallback,
-                  int transientStateDelayMs)
+                  DebugParameters debugParameters)
         : mCommandMQ(std::move(commandMQ)),
           mInternalCommandCookie(std::rand()),
           mReplyMQ(std::move(replyMQ)),
@@ -75,7 +84,7 @@
           mChannelLayout(channelLayout),
           mDataMQ(std::move(dataMQ)),
           mAsyncCallback(asyncCallback),
-          mTransientStateDelayMs(transientStateDelayMs) {}
+          mDebugParameters(debugParameters) {}
     StreamContext(StreamContext&& other)
         : mCommandMQ(std::move(other.mCommandMQ)),
           mInternalCommandCookie(other.mInternalCommandCookie),
@@ -83,8 +92,8 @@
           mFormat(other.mFormat),
           mChannelLayout(other.mChannelLayout),
           mDataMQ(std::move(other.mDataMQ)),
-          mAsyncCallback(other.mAsyncCallback),
-          mTransientStateDelayMs(other.mTransientStateDelayMs) {}
+          mAsyncCallback(std::move(other.mAsyncCallback)),
+          mDebugParameters(std::move(other.mDebugParameters)) {}
     StreamContext& operator=(StreamContext&& other) {
         mCommandMQ = std::move(other.mCommandMQ);
         mInternalCommandCookie = other.mInternalCommandCookie;
@@ -92,8 +101,8 @@
         mFormat = std::move(other.mFormat);
         mChannelLayout = std::move(other.mChannelLayout);
         mDataMQ = std::move(other.mDataMQ);
-        mAsyncCallback = other.mAsyncCallback;
-        mTransientStateDelayMs = other.mTransientStateDelayMs;
+        mAsyncCallback = std::move(other.mAsyncCallback);
+        mDebugParameters = std::move(other.mDebugParameters);
         return *this;
     }
 
@@ -107,10 +116,12 @@
     ::aidl::android::media::audio::common::AudioFormatDescription getFormat() const {
         return mFormat;
     }
+    bool getForceTransientBurst() const { return mDebugParameters.forceTransientBurst; }
+    bool getForceSynchronousDrain() const { return mDebugParameters.forceSynchronousDrain; }
     size_t getFrameSize() const;
     int getInternalCommandCookie() const { return mInternalCommandCookie; }
     ReplyMQ* getReplyMQ() const { return mReplyMQ.get(); }
-    int getTransientStateDelayMs() const { return mTransientStateDelayMs; }
+    int getTransientStateDelayMs() const { return mDebugParameters.transientStateDelayMs; }
     bool isValid() const;
     void reset();
 
@@ -122,7 +133,7 @@
     ::aidl::android::media::audio::common::AudioChannelLayout mChannelLayout;
     std::unique_ptr<DataMQ> mDataMQ;
     std::shared_ptr<IStreamCallback> mAsyncCallback;
-    int mTransientStateDelayMs;
+    DebugParameters mDebugParameters;
 };
 
 class StreamWorkerCommonLogic : public ::android::hardware::audio::common::StreamLogic {
@@ -141,7 +152,9 @@
           mReplyMQ(context.getReplyMQ()),
           mDataMQ(context.getDataMQ()),
           mAsyncCallback(context.getAsyncCallback()),
-          mTransientStateDelayMs(context.getTransientStateDelayMs()) {}
+          mTransientStateDelayMs(context.getTransientStateDelayMs()),
+          mForceTransientBurst(context.getForceTransientBurst()),
+          mForceSynchronousDrain(context.getForceSynchronousDrain()) {}
     std::string init() override;
     void populateReply(StreamDescriptor::Reply* reply, bool isConnected) const;
     void populateReplyWrongState(StreamDescriptor::Reply* reply,
@@ -164,6 +177,8 @@
     std::shared_ptr<IStreamCallback> mAsyncCallback;
     const std::chrono::duration<int, std::milli> mTransientStateDelayMs;
     std::chrono::time_point<std::chrono::steady_clock> mTransientStateStart;
+    const bool mForceTransientBurst;
+    const bool mForceSynchronousDrain;
     // We use an array and the "size" field instead of a vector to be able to detect
     // memory allocation issues.
     std::unique_ptr<int8_t[]> mDataBuffer;
diff --git a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
index 8da475e..d4f2811 100644
--- a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
@@ -18,6 +18,7 @@
 #include <chrono>
 #include <cmath>
 #include <condition_variable>
+#include <forward_list>
 #include <limits>
 #include <memory>
 #include <mutex>
@@ -85,6 +86,7 @@
 using aidl::android::media::audio::common::AudioPortExt;
 using aidl::android::media::audio::common::AudioSource;
 using aidl::android::media::audio::common::AudioUsage;
+using aidl::android::media::audio::common::Boolean;
 using aidl::android::media::audio::common::Float;
 using aidl::android::media::audio::common::Int;
 using aidl::android::media::audio::common::Void;
@@ -126,7 +128,7 @@
         return WithDebugFlags(parent.mFlags);
     }
 
-    WithDebugFlags() {}
+    WithDebugFlags() = default;
     explicit WithDebugFlags(const ModuleDebug& initial) : mInitial(initial), mFlags(initial) {}
     WithDebugFlags(const WithDebugFlags&) = delete;
     WithDebugFlags& operator=(const WithDebugFlags&) = delete;
@@ -135,7 +137,10 @@
             EXPECT_IS_OK(mModule->setModuleDebug(mInitial));
         }
     }
-    void SetUp(IModule* module) { ASSERT_IS_OK(module->setModuleDebug(mFlags)); }
+    void SetUp(IModule* module) {
+        ASSERT_IS_OK(module->setModuleDebug(mFlags));
+        mModule = module;
+    }
     ModuleDebug& flags() { return mFlags; }
 
   private:
@@ -144,13 +149,65 @@
     IModule* mModule = nullptr;
 };
 
+template <typename T>
+class WithModuleParameter {
+  public:
+    WithModuleParameter(const std::string parameterId, const T& value)
+        : mParameterId(parameterId), mValue(value) {}
+    WithModuleParameter(const WithModuleParameter&) = delete;
+    WithModuleParameter& operator=(const WithModuleParameter&) = delete;
+    ~WithModuleParameter() {
+        if (mModule != nullptr) {
+            VendorParameter parameter{.id = mParameterId};
+            parameter.ext.setParcelable(mInitial);
+            EXPECT_IS_OK(mModule->setVendorParameters({parameter}, false));
+        }
+    }
+    ScopedAStatus SetUpNoChecks(IModule* module, bool failureExpected) {
+        std::vector<VendorParameter> parameters;
+        ScopedAStatus result = module->getVendorParameters({mParameterId}, &parameters);
+        if (result.isOk() && parameters.size() == 1) {
+            std::optional<T> maybeInitial;
+            binder_status_t status = parameters[0].ext.getParcelable(&maybeInitial);
+            if (status == STATUS_OK && maybeInitial.has_value()) {
+                mInitial = maybeInitial.value();
+                VendorParameter parameter{.id = mParameterId};
+                parameter.ext.setParcelable(mValue);
+                result = module->setVendorParameters({parameter}, false);
+                if (result.isOk()) {
+                    LOG(INFO) << __func__ << ": overriding parameter \"" << mParameterId
+                              << "\" with " << mValue.toString()
+                              << ", old value: " << mInitial.toString();
+                    mModule = module;
+                }
+            } else {
+                LOG(ERROR) << __func__ << ": error while retrieving the value of \"" << mParameterId
+                           << "\"";
+                return ScopedAStatus::fromStatus(status);
+            }
+        }
+        if (!result.isOk()) {
+            LOG(failureExpected ? INFO : ERROR)
+                    << __func__ << ": can not override vendor parameter \"" << mParameterId << "\""
+                    << result;
+        }
+        return result;
+    }
+
+  private:
+    const std::string mParameterId;
+    const T mValue;
+    IModule* mModule = nullptr;
+    T mInitial;
+};
+
 // For consistency, WithAudioPortConfig can start both with a non-existent
 // port config, and with an existing one. Existence is determined by the
 // id of the provided config. If it's not 0, then WithAudioPortConfig is
 // essentially a no-op wrapper.
 class WithAudioPortConfig {
   public:
-    WithAudioPortConfig() {}
+    WithAudioPortConfig() = default;
     explicit WithAudioPortConfig(const AudioPortConfig& config) : mInitialConfig(config) {}
     WithAudioPortConfig(const WithAudioPortConfig&) = delete;
     WithAudioPortConfig& operator=(const WithAudioPortConfig&) = delete;
@@ -302,26 +359,31 @@
 
     void SetUpImpl(const std::string& moduleName) {
         ASSERT_NO_FATAL_FAILURE(ConnectToService(moduleName));
-        debug.flags().simulateDeviceConnections = true;
-        ASSERT_NO_FATAL_FAILURE(debug.SetUp(module.get()));
     }
 
-    void TearDownImpl() {
-        if (module != nullptr) {
-            EXPECT_IS_OK(module->setModuleDebug(ModuleDebug{}));
-        }
-    }
+    void TearDownImpl() { debug.reset(); }
 
     void ConnectToService(const std::string& moduleName) {
+        ASSERT_EQ(module, nullptr);
+        ASSERT_EQ(debug, nullptr);
         module = IModule::fromBinder(binderUtil.connectToService(moduleName));
         ASSERT_NE(module, nullptr);
+        ASSERT_NO_FATAL_FAILURE(SetUpDebug());
     }
 
     void RestartService() {
         ASSERT_NE(module, nullptr);
         moduleConfig.reset();
+        debug.reset();
         module = IModule::fromBinder(binderUtil.restartService());
         ASSERT_NE(module, nullptr);
+        ASSERT_NO_FATAL_FAILURE(SetUpDebug());
+    }
+
+    void SetUpDebug() {
+        debug.reset(new WithDebugFlags());
+        debug->flags().simulateDeviceConnections = true;
+        ASSERT_NO_FATAL_FAILURE(debug->SetUp(module.get()));
     }
 
     void ApplyEveryConfig(const std::vector<AudioPortConfig>& configs) {
@@ -390,7 +452,7 @@
     std::shared_ptr<IModule> module;
     std::unique_ptr<ModuleConfig> moduleConfig;
     AudioHalBinderServiceUtil binderUtil;
-    WithDebugFlags debug;
+    std::unique_ptr<WithDebugFlags> debug;
 };
 
 class AudioCoreModule : public AudioCoreModuleBase, public testing::TestWithParam<std::string> {
@@ -465,6 +527,7 @@
     size_t getBufferSizeFrames() const { return mBufferSizeFrames; }
     CommandMQ* getCommandMQ() const { return mCommandMQ.get(); }
     DataMQ* getDataMQ() const { return mDataMQ.get(); }
+    size_t getFrameSizeBytes() const { return mFrameSizeBytes; }
     ReplyMQ* getReplyMQ() const { return mReplyMQ.get(); }
 
   private:
@@ -504,10 +567,48 @@
     return std::to_string(static_cast<int32_t>(event));
 }
 
+// Note: we use a reference wrapper, not a pointer, because methods of std::*list
+// return references to inserted elements. This way, we can put a returned reference
+// into the children vector without any type conversions, and this makes DAG creation
+// code more clear.
+template <typename T>
+struct DagNode : public std::pair<T, std::vector<std::reference_wrapper<DagNode<T>>>> {
+    using Children = std::vector<std::reference_wrapper<DagNode>>;
+    DagNode(const T& t, const Children& c) : std::pair<T, Children>(t, c) {}
+    DagNode(T&& t, Children&& c) : std::pair<T, Children>(std::move(t), std::move(c)) {}
+    const T& datum() const { return this->first; }
+    Children& children() { return this->second; }
+    const Children& children() const { return this->second; }
+};
+// Since DagNodes do contain references to next nodes, node links provided
+// by the list are not used. Thus, the order of the nodes in the list is not
+// important, except that the starting node must be at the front of the list,
+// which means, it must always be added last.
+template <typename T>
+struct Dag : public std::forward_list<DagNode<T>> {
+    Dag() = default;
+    // We prohibit copying and moving Dag instances because implementing that
+    // is not trivial due to references between nodes.
+    Dag(const Dag&) = delete;
+    Dag(Dag&&) = delete;
+    Dag& operator=(const Dag&) = delete;
+    Dag& operator=(Dag&&) = delete;
+};
+
 // Transition to the next state happens either due to a command from the client,
 // or after an event received from the server.
 using TransitionTrigger = std::variant<StreamDescriptor::Command, StreamEventReceiver::Event>;
-using StateTransition = std::pair<TransitionTrigger, StreamDescriptor::State>;
+std::string toString(const TransitionTrigger& trigger) {
+    if (std::holds_alternative<StreamDescriptor::Command>(trigger)) {
+        return std::string("'")
+                .append(toString(std::get<StreamDescriptor::Command>(trigger).getTag()))
+                .append("' command");
+    }
+    return std::string("'")
+            .append(toString(std::get<StreamEventReceiver::Event>(trigger)))
+            .append("' event");
+}
+
 struct StateSequence {
     virtual ~StateSequence() = default;
     virtual void rewind() = 0;
@@ -517,6 +618,10 @@
     virtual void advance(StreamDescriptor::State state) = 0;
 };
 
+// Defines the current state and the trigger to transfer to the next one,
+// thus "state" is the "from" state.
+using StateTransitionFrom = std::pair<StreamDescriptor::State, TransitionTrigger>;
+
 static const StreamDescriptor::Command kGetStatusCommand =
         StreamDescriptor::Command::make<StreamDescriptor::Command::Tag::getStatus>(Void{});
 static const StreamDescriptor::Command kStartCommand =
@@ -542,66 +647,65 @@
         StreamEventReceiver::Event::TransferReady;
 static const StreamEventReceiver::Event kDrainReadyEvent = StreamEventReceiver::Event::DrainReady;
 
-// Handle possible bifurcations:
-//   - on burst and on start: 'TRANSFERRING' -> {'ACTIVE', 'TRANSFERRING'}
-//   - on pause: 'TRANSFER_PAUSED' -> {'PAUSED', 'TRANSFER_PAUSED'}
-// It is assumed that the 'steps' provided on the construction contain the sequence
-// for the async case, which gets corrected in the case when the HAL decided to do
-// a synchronous transfer.
-class SmartStateSequence : public StateSequence {
+struct StateDag : public Dag<StateTransitionFrom> {
+    using Node = StateDag::reference;
+    using NextStates = StateDag::value_type::Children;
+
+    template <typename... Next>
+    Node makeNode(StreamDescriptor::State s, TransitionTrigger t, Next&&... next) {
+        return emplace_front(std::make_pair(s, t), NextStates{std::forward<Next>(next)...});
+    }
+    Node makeNodes(const std::vector<StateTransitionFrom>& v, Node last) {
+        auto helper = [&](auto i, auto&& h) -> Node {
+            if (i == v.end()) return last;
+            return makeNode(i->first, i->second, h(++i, h));
+        };
+        return helper(v.begin(), helper);
+    }
+    Node makeNodes(const std::vector<StateTransitionFrom>& v, StreamDescriptor::State f) {
+        return makeNodes(v, makeFinalNode(f));
+    }
+    Node makeFinalNode(StreamDescriptor::State s) {
+        // The actual command used here is irrelevant. Since it's the final node
+        // in the test sequence, no commands sent after reaching it.
+        return emplace_front(std::make_pair(s, kGetStatusCommand), NextStates{});
+    }
+};
+
+class StateSequenceFollower : public StateSequence {
   public:
-    explicit SmartStateSequence(const std::vector<StateTransition>& steps) : mSteps(steps) {}
-    explicit SmartStateSequence(std::vector<StateTransition>&& steps) : mSteps(std::move(steps)) {}
-    void rewind() override { mCurrentStep = 0; }
-    bool done() const override { return mCurrentStep >= mSteps.size(); }
-    TransitionTrigger getTrigger() override { return mSteps[mCurrentStep].first; }
+    explicit StateSequenceFollower(std::unique_ptr<StateDag> steps)
+        : mSteps(std::move(steps)), mCurrent(mSteps->front()) {}
+    void rewind() override { mCurrent = mSteps->front(); }
+    bool done() const override { return current().children().empty(); }
+    TransitionTrigger getTrigger() override { return current().datum().second; }
     std::set<StreamDescriptor::State> getExpectedStates() override {
-        std::set<StreamDescriptor::State> result = {getState()};
-        if (isBurstBifurcation() || isStartBifurcation()) {
-            result.insert(StreamDescriptor::State::ACTIVE);
-        } else if (isPauseBifurcation()) {
-            result.insert(StreamDescriptor::State::PAUSED);
-        }
+        std::set<StreamDescriptor::State> result;
+        std::transform(current().children().cbegin(), current().children().cend(),
+                       std::inserter(result, result.begin()),
+                       [](const auto& node) { return node.get().datum().first; });
+        LOG(DEBUG) << __func__ << ": " << ::android::internal::ToString(result);
         return result;
     }
     void advance(StreamDescriptor::State state) override {
-        if (isBurstBifurcation() && state == StreamDescriptor::State::ACTIVE &&
-            mCurrentStep + 1 < mSteps.size() &&
-            mSteps[mCurrentStep + 1].first == TransitionTrigger{kTransferReadyEvent}) {
-            mCurrentStep++;
+        if (auto it = std::find_if(
+                    current().children().cbegin(), current().children().cend(),
+                    [&](const auto& node) { return node.get().datum().first == state; });
+            it != current().children().cend()) {
+            LOG(DEBUG) << __func__ << ": " << toString(mCurrent.get().datum().first) << " -> "
+                       << toString(it->get().datum().first);
+            mCurrent = *it;
+        } else {
+            LOG(FATAL) << __func__ << ": state " << toString(state) << " is unexpected";
         }
-        mCurrentStep++;
     }
 
   private:
-    StreamDescriptor::State getState() const { return mSteps[mCurrentStep].second; }
-    bool isBurstBifurcation() {
-        return getTrigger() == TransitionTrigger{kBurstCommand} &&
-               getState() == StreamDescriptor::State::TRANSFERRING;
-    }
-    bool isPauseBifurcation() {
-        return getTrigger() == TransitionTrigger{kPauseCommand} &&
-               getState() == StreamDescriptor::State::TRANSFER_PAUSED;
-    }
-    bool isStartBifurcation() {
-        return getTrigger() == TransitionTrigger{kStartCommand} &&
-               getState() == StreamDescriptor::State::TRANSFERRING;
-    }
-    const std::vector<StateTransition> mSteps;
-    size_t mCurrentStep = 0;
+    StateDag::const_reference current() const { return mCurrent.get(); }
+    std::unique_ptr<StateDag> mSteps;
+    std::reference_wrapper<StateDag::value_type> mCurrent;
 };
 
-std::string toString(const TransitionTrigger& trigger) {
-    if (std::holds_alternative<StreamDescriptor::Command>(trigger)) {
-        return std::string("'")
-                .append(toString(std::get<StreamDescriptor::Command>(trigger).getTag()))
-                .append("' command");
-    }
-    return std::string("'")
-            .append(toString(std::get<StreamEventReceiver::Event>(trigger)))
-            .append("' event");
-}
-
 struct StreamLogicDriver {
     virtual ~StreamLogicDriver() = default;
     // Return 'true' to stop the worker.
@@ -927,7 +1031,7 @@
         return common->close();
     }
 
-    WithStream() {}
+    WithStream() = default;
     explicit WithStream(const AudioPortConfig& portConfig) : mPortConfig(portConfig) {}
     WithStream(const WithStream&) = delete;
     WithStream& operator=(const WithStream&) = delete;
@@ -1032,7 +1136,7 @@
 
 class WithAudioPatch {
   public:
-    WithAudioPatch() {}
+    WithAudioPatch() = default;
     WithAudioPatch(const AudioPortConfig& srcPortConfig, const AudioPortConfig& sinkPortConfig)
         : mSrcPortConfig(srcPortConfig), mSinkPortConfig(sinkPortConfig) {}
     WithAudioPatch(bool sinkIsCfg1, const AudioPortConfig& portConfig1,
@@ -1473,7 +1577,7 @@
         GTEST_SKIP() << "No external devices in the module.";
     }
     AudioPort ignored;
-    WithDebugFlags doNotSimulateConnections = WithDebugFlags::createNested(debug);
+    WithDebugFlags doNotSimulateConnections = WithDebugFlags::createNested(*debug);
     doNotSimulateConnections.flags().simulateDeviceConnections = false;
     ASSERT_NO_FATAL_FAILURE(doNotSimulateConnections.SetUp(module.get()));
     for (const auto& port : ports) {
@@ -1493,7 +1597,7 @@
     }
     WithDevicePortConnectedState portConnected(*ports.begin(), GenerateUniqueDeviceAddress());
     ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get()));
-    ModuleDebug midwayDebugChange = debug.flags();
+    ModuleDebug midwayDebugChange = debug->flags();
     midwayDebugChange.simulateDeviceConnections = false;
     EXPECT_STATUS(EX_ILLEGAL_STATE, module->setModuleDebug(midwayDebugChange))
             << "when trying to disable connections simulation while having a connected device";
@@ -2716,8 +2820,8 @@
 
 class StreamLogicDefaultDriver : public StreamLogicDriver {
   public:
-    explicit StreamLogicDefaultDriver(std::shared_ptr<StateSequence> commands)
-        : mCommands(commands) {
+    StreamLogicDefaultDriver(std::shared_ptr<StateSequence> commands, size_t frameSizeBytes)
+        : mCommands(commands), mFrameSizeBytes(frameSizeBytes) {
         mCommands->rewind();
     }
 
@@ -2736,7 +2840,10 @@
                 if (actualSize != nullptr) {
                     // In the output scenario, reduce slightly the fmqByteCount to verify
                     // that the HAL module always consumes all data from the MQ.
-                    if (maxDataSize > 1) maxDataSize--;
+                    if (maxDataSize > static_cast<int>(mFrameSizeBytes)) {
+                        LOG(DEBUG) << __func__ << ": reducing data size by " << mFrameSizeBytes;
+                        maxDataSize -= mFrameSizeBytes;
+                    }
                     *actualSize = maxDataSize;
                 }
                 command->set<StreamDescriptor::Command::Tag::burst>(maxDataSize);
@@ -2782,6 +2889,7 @@
 
   protected:
     std::shared_ptr<StateSequence> mCommands;
+    const size_t mFrameSizeBytes;
     std::optional<StreamDescriptor::State> mPreviousState;
     std::optional<int64_t> mPreviousFrames;
     bool mObservablePositionIncrease = false;
@@ -2830,7 +2938,7 @@
                 (!isNonBlocking && streamType == StreamTypeFilter::ASYNC)) {
                 continue;
             }
-            WithDebugFlags delayTransientStates = WithDebugFlags::createNested(debug);
+            WithDebugFlags delayTransientStates = WithDebugFlags::createNested(*debug);
             delayTransientStates.flags().streamTransientStateDelayMs =
                     std::get<NAMED_CMD_DELAY_MS>(std::get<PARAM_CMD_SEQ>(GetParam()));
             ASSERT_NO_FATAL_FAILURE(delayTransientStates.SetUp(module.get()));
@@ -2841,6 +2949,40 @@
             } else {
                 ASSERT_NO_FATAL_FAILURE(RunStreamIoCommandsImplSeq2(portConfig, commandsAndStates));
             }
+            if (isNonBlocking) {
+                // Also try running the same sequence with "aosp.forceTransientBurst" set.
+                // This will only work with the default implementation. When it works, the stream
+                // tries always to move to the 'TRANSFERRING' state after a burst.
+                // This helps to check more paths for our test scenarios.
+                WithModuleParameter forceTransientBurst("aosp.forceTransientBurst", Boolean{true});
+                if (forceTransientBurst.SetUpNoChecks(module.get(), true /*failureExpected*/)
+                            .isOk()) {
+                    if (!std::get<PARAM_SETUP_SEQ>(GetParam())) {
+                        ASSERT_NO_FATAL_FAILURE(
+                                RunStreamIoCommandsImplSeq1(portConfig, commandsAndStates));
+                    } else {
+                        ASSERT_NO_FATAL_FAILURE(
+                                RunStreamIoCommandsImplSeq2(portConfig, commandsAndStates));
+                    }
+                }
+            } else if (!IOTraits<Stream>::is_input) {
+                // Also try running the same sequence with "aosp.forceSynchronousDrain" set.
+                // This will only work with the default implementation. When it works, the stream
+                // tries always to move to the 'IDLE' state after a drain.
+                // This helps to check more paths for our test scenarios.
+                WithModuleParameter forceSynchronousDrain("aosp.forceSynchronousDrain",
+                                                          Boolean{true});
+                if (forceSynchronousDrain.SetUpNoChecks(module.get(), true /*failureExpected*/)
+                            .isOk()) {
+                    if (!std::get<PARAM_SETUP_SEQ>(GetParam())) {
+                        ASSERT_NO_FATAL_FAILURE(
+                                RunStreamIoCommandsImplSeq1(portConfig, commandsAndStates));
+                    } else {
+                        ASSERT_NO_FATAL_FAILURE(
+                                RunStreamIoCommandsImplSeq2(portConfig, commandsAndStates));
+                    }
+                }
+            }
         }
     }
 
@@ -2861,7 +3003,8 @@
 
         WithStream<Stream> stream(patch.getPortConfig(IOTraits<Stream>::is_input));
         ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
-        StreamLogicDefaultDriver driver(commandsAndStates);
+        StreamLogicDefaultDriver driver(commandsAndStates,
+                                        stream.getContext()->getFrameSizeBytes());
         typename IOTraits<Stream>::Worker worker(*stream.getContext(), &driver,
                                                  stream.getEventReceiver());
 
@@ -2882,7 +3025,8 @@
                                      std::shared_ptr<StateSequence> commandsAndStates) {
         WithStream<Stream> stream(portConfig);
         ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames));
-        StreamLogicDefaultDriver driver(commandsAndStates);
+        StreamLogicDefaultDriver driver(commandsAndStates,
+                                        stream.getContext()->getFrameSizeBytes());
         typename IOTraits<Stream>::Worker worker(*stream.getContext(), &driver,
                                                  stream.getEventReceiver());
 
@@ -3219,38 +3363,52 @@
 
 // TODO: Add async test cases for input once it is implemented.
 
-std::shared_ptr<StateSequence> makeBurstCommands(bool isSync, size_t burstCount) {
-    const auto burst =
-            isSync ? std::vector<StateTransition>{std::make_pair(kBurstCommand,
-                                                                 StreamDescriptor::State::ACTIVE)}
-                   : std::vector<StateTransition>{
-                             std::make_pair(kBurstCommand, StreamDescriptor::State::TRANSFERRING),
-                             std::make_pair(kTransferReadyEvent, StreamDescriptor::State::ACTIVE)};
-    std::vector<StateTransition> result{
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE)};
-    for (size_t i = 0; i < burstCount; ++i) {
-        result.insert(result.end(), burst.begin(), burst.end());
+std::shared_ptr<StateSequence> makeBurstCommands(bool isSync) {
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    StateDag::Node last = d->makeFinalNode(State::ACTIVE);
+    StateDag::Node active = d->makeNode(State::ACTIVE, kBurstCommand, last);
+    StateDag::Node idle = d->makeNode(State::IDLE, kBurstCommand, active);
+    if (!isSync) {
+        // Allow optional routing via the TRANSFERRING state on bursts.
+        active.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, last));
+        idle.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, active));
     }
-    return std::make_shared<SmartStateSequence>(result);
+    d->makeNode(State::STANDBY, kStartCommand, idle);
+    return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kReadSeq =
-        std::make_tuple(std::string("Read"), 0, StreamTypeFilter::ANY, makeBurstCommands(true, 3));
-static const NamedCommandSequence kWriteSyncSeq = std::make_tuple(
-        std::string("Write"), 0, StreamTypeFilter::SYNC, makeBurstCommands(true, 3));
-static const NamedCommandSequence kWriteAsyncSeq = std::make_tuple(
-        std::string("Write"), 0, StreamTypeFilter::ASYNC, makeBurstCommands(false, 3));
+        std::make_tuple(std::string("Read"), 0, StreamTypeFilter::ANY, makeBurstCommands(true));
+static const NamedCommandSequence kWriteSyncSeq =
+        std::make_tuple(std::string("Write"), 0, StreamTypeFilter::SYNC, makeBurstCommands(true));
+static const NamedCommandSequence kWriteAsyncSeq =
+        std::make_tuple(std::string("Write"), 0, StreamTypeFilter::ASYNC, makeBurstCommands(false));
 
 std::shared_ptr<StateSequence> makeAsyncDrainCommands(bool isInput) {
-    return std::make_shared<SmartStateSequence>(std::vector<StateTransition>{
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-            std::make_pair(kBurstCommand, isInput ? StreamDescriptor::State::ACTIVE
-                                                  : StreamDescriptor::State::TRANSFERRING),
-            std::make_pair(isInput ? kDrainInCommand : kDrainOutAllCommand,
-                           StreamDescriptor::State::DRAINING),
-            isInput ? std::make_pair(kStartCommand, StreamDescriptor::State::ACTIVE)
-                    : std::make_pair(kBurstCommand, StreamDescriptor::State::TRANSFERRING),
-            std::make_pair(isInput ? kDrainInCommand : kDrainOutAllCommand,
-                           StreamDescriptor::State::DRAINING)});
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    if (isInput) {
+        d->makeNodes({std::make_pair(State::STANDBY, kStartCommand),
+                      std::make_pair(State::IDLE, kBurstCommand),
+                      std::make_pair(State::ACTIVE, kDrainInCommand),
+                      std::make_pair(State::DRAINING, kStartCommand),
+                      std::make_pair(State::ACTIVE, kDrainInCommand)},
+                     State::DRAINING);
+    } else {
+        StateDag::Node draining =
+                d->makeNodes({std::make_pair(State::DRAINING, kBurstCommand),
+                              std::make_pair(State::TRANSFERRING, kDrainOutAllCommand)},
+                             State::DRAINING);
+        StateDag::Node idle =
+                d->makeNodes({std::make_pair(State::IDLE, kBurstCommand),
+                              std::make_pair(State::TRANSFERRING, kDrainOutAllCommand)},
+                             draining);
+        // If we get straight into ACTIVE on burst, no further testing is possible.
+        draining.children().push_back(d->makeFinalNode(State::ACTIVE));
+        idle.children().push_back(d->makeFinalNode(State::ACTIVE));
+        d->makeNode(State::STANDBY, kStartCommand, idle);
+    }
+    return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kWriteDrainAsyncSeq =
         std::make_tuple(std::string("WriteDrain"), kStreamTransientStateTransitionDelayMs,
@@ -3259,58 +3417,92 @@
         std::string("Drain"), 0, StreamTypeFilter::ANY, makeAsyncDrainCommands(true));
 
 std::shared_ptr<StateSequence> makeDrainOutCommands(bool isSync) {
-    return std::make_shared<SmartStateSequence>(std::vector<StateTransition>{
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-            std::make_pair(kBurstCommand, StreamDescriptor::State::ACTIVE),
-            std::make_pair(kDrainOutAllCommand, StreamDescriptor::State::DRAINING),
-            std::make_pair(isSync ? TransitionTrigger(kGetStatusCommand)
-                                  : TransitionTrigger(kDrainReadyEvent),
-                           StreamDescriptor::State::IDLE)});
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    StateDag::Node last = d->makeFinalNode(State::IDLE);
+    StateDag::Node active = d->makeNodes(
+            {std::make_pair(State::ACTIVE, kDrainOutAllCommand),
+             std::make_pair(State::DRAINING, isSync ? TransitionTrigger(kGetStatusCommand)
+                                                    : TransitionTrigger(kDrainReadyEvent))},
+            last);
+    StateDag::Node idle = d->makeNode(State::IDLE, kBurstCommand, active);
+    if (!isSync) {
+        idle.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, active));
+    } else {
+        active.children().push_back(last);
+    }
+    d->makeNode(State::STANDBY, kStartCommand, idle);
+    return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kDrainOutSyncSeq = std::make_tuple(
         std::string("Drain"), 0, StreamTypeFilter::SYNC, makeDrainOutCommands(true));
 static const NamedCommandSequence kDrainOutAsyncSeq = std::make_tuple(
         std::string("Drain"), 0, StreamTypeFilter::ASYNC, makeDrainOutCommands(false));
 
-std::shared_ptr<StateSequence> makeDrainOutPauseCommands(bool isSync) {
-    return std::make_shared<SmartStateSequence>(std::vector<StateTransition>{
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-            std::make_pair(kBurstCommand, isSync ? StreamDescriptor::State::ACTIVE
-                                                 : StreamDescriptor::State::TRANSFERRING),
-            std::make_pair(kDrainOutAllCommand, StreamDescriptor::State::DRAINING),
-            std::make_pair(kPauseCommand, StreamDescriptor::State::DRAIN_PAUSED),
-            std::make_pair(kStartCommand, StreamDescriptor::State::DRAINING),
-            std::make_pair(kPauseCommand, StreamDescriptor::State::DRAIN_PAUSED),
-            std::make_pair(kBurstCommand, isSync ? StreamDescriptor::State::PAUSED
-                                                 : StreamDescriptor::State::TRANSFER_PAUSED)});
+std::shared_ptr<StateSequence> makeDrainPauseOutCommands(bool isSync) {
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    StateDag::Node draining = d->makeNodes({std::make_pair(State::DRAINING, kPauseCommand),
+                                            std::make_pair(State::DRAIN_PAUSED, kStartCommand),
+                                            std::make_pair(State::DRAINING, kPauseCommand),
+                                            std::make_pair(State::DRAIN_PAUSED, kBurstCommand)},
+                                           isSync ? State::PAUSED : State::TRANSFER_PAUSED);
+    StateDag::Node active = d->makeNode(State::ACTIVE, kDrainOutAllCommand, draining);
+    StateDag::Node idle = d->makeNode(State::IDLE, kBurstCommand, active);
+    if (!isSync) {
+        idle.children().push_back(d->makeNode(State::TRANSFERRING, kDrainOutAllCommand, draining));
+    } else {
+        // If we get straight into IDLE on drain, no further testing is possible.
+        active.children().push_back(d->makeFinalNode(State::IDLE));
+    }
+    d->makeNode(State::STANDBY, kStartCommand, idle);
+    return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kDrainPauseOutSyncSeq =
         std::make_tuple(std::string("DrainPause"), kStreamTransientStateTransitionDelayMs,
-                        StreamTypeFilter::SYNC, makeDrainOutPauseCommands(true));
+                        StreamTypeFilter::SYNC, makeDrainPauseOutCommands(true));
 static const NamedCommandSequence kDrainPauseOutAsyncSeq =
         std::make_tuple(std::string("DrainPause"), kStreamTransientStateTransitionDelayMs,
-                        StreamTypeFilter::ASYNC, makeDrainOutPauseCommands(false));
+                        StreamTypeFilter::ASYNC, makeDrainPauseOutCommands(false));
 
 // This sequence also verifies that the capture / presentation position is not reset on standby.
 std::shared_ptr<StateSequence> makeStandbyCommands(bool isInput, bool isSync) {
-    return std::make_shared<SmartStateSequence>(std::vector<StateTransition>{
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-            std::make_pair(kStandbyCommand, StreamDescriptor::State::STANDBY),
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-            std::make_pair(kBurstCommand, isInput || isSync
-                                                  ? StreamDescriptor::State::ACTIVE
-                                                  : StreamDescriptor::State::TRANSFERRING),
-            std::make_pair(kPauseCommand, isInput || isSync
-                                                  ? StreamDescriptor::State::PAUSED
-                                                  : StreamDescriptor::State::TRANSFER_PAUSED),
-            std::make_pair(kFlushCommand, isInput ? StreamDescriptor::State::STANDBY
-                                                  : StreamDescriptor::State::IDLE),
-            std::make_pair(isInput ? kGetStatusCommand : kStandbyCommand,  // no-op for input
-                           StreamDescriptor::State::STANDBY),
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-            std::make_pair(kBurstCommand, isInput || isSync
-                                                  ? StreamDescriptor::State::ACTIVE
-                                                  : StreamDescriptor::State::TRANSFERRING)});
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    if (isInput) {
+        d->makeNodes({std::make_pair(State::STANDBY, kStartCommand),
+                      std::make_pair(State::IDLE, kStandbyCommand),
+                      std::make_pair(State::STANDBY, kStartCommand),
+                      std::make_pair(State::IDLE, kBurstCommand),
+                      std::make_pair(State::ACTIVE, kPauseCommand),
+                      std::make_pair(State::PAUSED, kFlushCommand),
+                      std::make_pair(State::STANDBY, kStartCommand),
+                      std::make_pair(State::IDLE, kBurstCommand)},
+                     State::ACTIVE);
+    } else {
+        StateDag::Node idle3 =
+                d->makeNode(State::IDLE, kBurstCommand, d->makeFinalNode(State::ACTIVE));
+        StateDag::Node idle2 = d->makeNodes({std::make_pair(State::IDLE, kStandbyCommand),
+                                             std::make_pair(State::STANDBY, kStartCommand)},
+                                            idle3);
+        StateDag::Node active = d->makeNodes({std::make_pair(State::ACTIVE, kPauseCommand),
+                                              std::make_pair(State::PAUSED, kFlushCommand)},
+                                             idle2);
+        StateDag::Node idle = d->makeNode(State::IDLE, kBurstCommand, active);
+        if (!isSync) {
+            idle3.children().push_back(d->makeFinalNode(State::TRANSFERRING));
+            StateDag::Node transferring =
+                    d->makeNodes({std::make_pair(State::TRANSFERRING, kPauseCommand),
+                                  std::make_pair(State::TRANSFER_PAUSED, kFlushCommand)},
+                                 idle2);
+            idle.children().push_back(transferring);
+        }
+        d->makeNodes({std::make_pair(State::STANDBY, kStartCommand),
+                      std::make_pair(State::IDLE, kStandbyCommand),
+                      std::make_pair(State::STANDBY, kStartCommand)},
+                     idle);
+    }
+    return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kStandbyInSeq = std::make_tuple(
         std::string("Standby"), 0, StreamTypeFilter::ANY, makeStandbyCommands(true, false));
@@ -3320,50 +3512,71 @@
         std::make_tuple(std::string("Standby"), kStreamTransientStateTransitionDelayMs,
                         StreamTypeFilter::ASYNC, makeStandbyCommands(false, false));
 
-static const NamedCommandSequence kPauseInSeq =
-        std::make_tuple(std::string("Pause"), 0, StreamTypeFilter::ANY,
-                        std::make_shared<SmartStateSequence>(std::vector<StateTransition>{
-                                std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-                                std::make_pair(kBurstCommand, StreamDescriptor::State::ACTIVE),
-                                std::make_pair(kPauseCommand, StreamDescriptor::State::PAUSED),
-                                std::make_pair(kBurstCommand, StreamDescriptor::State::ACTIVE),
-                                std::make_pair(kPauseCommand, StreamDescriptor::State::PAUSED),
-                                std::make_pair(kFlushCommand, StreamDescriptor::State::STANDBY)}));
-static const NamedCommandSequence kPauseOutSyncSeq =
-        std::make_tuple(std::string("Pause"), 0, StreamTypeFilter::SYNC,
-                        std::make_shared<SmartStateSequence>(std::vector<StateTransition>{
-                                std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-                                std::make_pair(kBurstCommand, StreamDescriptor::State::ACTIVE),
-                                std::make_pair(kPauseCommand, StreamDescriptor::State::PAUSED),
-                                std::make_pair(kStartCommand, StreamDescriptor::State::ACTIVE),
-                                std::make_pair(kPauseCommand, StreamDescriptor::State::PAUSED),
-                                std::make_pair(kBurstCommand, StreamDescriptor::State::PAUSED),
-                                std::make_pair(kStartCommand, StreamDescriptor::State::ACTIVE),
-                                std::make_pair(kPauseCommand, StreamDescriptor::State::PAUSED)}));
-/* TODO: Figure out a better way for testing sync/async bursts
-static const NamedCommandSequence kPauseOutAsyncSeq = std::make_tuple(
-        std::string("Pause"), kStreamTransientStateTransitionDelayMs, StreamTypeFilter::ASYNC,
-        std::make_shared<StaticStateSequence>(std::vector<StateTransition>{
-                std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-                std::make_pair(kBurstCommand, StreamDescriptor::State::TRANSFERRING),
-                std::make_pair(kPauseCommand, StreamDescriptor::State::TRANSFER_PAUSED),
-                std::make_pair(kStartCommand, StreamDescriptor::State::TRANSFERRING),
-                std::make_pair(kPauseCommand, StreamDescriptor::State::TRANSFER_PAUSED),
-                std::make_pair(kDrainOutAllCommand, StreamDescriptor::State::DRAIN_PAUSED),
-                std::make_pair(kBurstCommand, StreamDescriptor::State::TRANSFER_PAUSED)}));
-*/
+std::shared_ptr<StateSequence> makePauseCommands(bool isInput, bool isSync) {
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    if (isInput) {
+        d->makeNodes({std::make_pair(State::STANDBY, kStartCommand),
+                      std::make_pair(State::IDLE, kBurstCommand),
+                      std::make_pair(State::ACTIVE, kPauseCommand),
+                      std::make_pair(State::PAUSED, kBurstCommand),
+                      std::make_pair(State::ACTIVE, kPauseCommand),
+                      std::make_pair(State::PAUSED, kFlushCommand)},
+                     State::STANDBY);
+    } else {
+        StateDag::Node idle = d->makeNodes({std::make_pair(State::IDLE, kBurstCommand),
+                                            std::make_pair(State::ACTIVE, kPauseCommand),
+                                            std::make_pair(State::PAUSED, kStartCommand),
+                                            std::make_pair(State::ACTIVE, kPauseCommand),
+                                            std::make_pair(State::PAUSED, kBurstCommand),
+                                            std::make_pair(State::PAUSED, kStartCommand),
+                                            std::make_pair(State::ACTIVE, kPauseCommand)},
+                                           State::PAUSED);
+        if (!isSync) {
+            idle.children().push_back(
+                    d->makeNodes({std::make_pair(State::TRANSFERRING, kPauseCommand),
+                                  std::make_pair(State::TRANSFER_PAUSED, kStartCommand),
+                                  std::make_pair(State::TRANSFERRING, kPauseCommand),
+                                  std::make_pair(State::TRANSFER_PAUSED, kDrainOutAllCommand),
+                                  std::make_pair(State::DRAIN_PAUSED, kBurstCommand)},
+                                 State::TRANSFER_PAUSED));
+        }
+        d->makeNode(State::STANDBY, kStartCommand, idle);
+    }
+    return std::make_shared<StateSequenceFollower>(std::move(d));
+}
+static const NamedCommandSequence kPauseInSeq = std::make_tuple(
+        std::string("Pause"), 0, StreamTypeFilter::ANY, makePauseCommands(true, false));
+static const NamedCommandSequence kPauseOutSyncSeq = std::make_tuple(
+        std::string("Pause"), 0, StreamTypeFilter::SYNC, makePauseCommands(false, true));
+static const NamedCommandSequence kPauseOutAsyncSeq =
+        std::make_tuple(std::string("Pause"), kStreamTransientStateTransitionDelayMs,
+                        StreamTypeFilter::ASYNC, makePauseCommands(false, false));
 
 std::shared_ptr<StateSequence> makeFlushCommands(bool isInput, bool isSync) {
-    return std::make_shared<SmartStateSequence>(std::vector<StateTransition>{
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-            std::make_pair(kBurstCommand, isInput || isSync
-                                                  ? StreamDescriptor::State::ACTIVE
-                                                  : StreamDescriptor::State::TRANSFERRING),
-            std::make_pair(kPauseCommand, isInput || isSync
-                                                  ? StreamDescriptor::State::PAUSED
-                                                  : StreamDescriptor::State::TRANSFER_PAUSED),
-            std::make_pair(kFlushCommand, isInput ? StreamDescriptor::State::STANDBY
-                                                  : StreamDescriptor::State::IDLE)});
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    if (isInput) {
+        d->makeNodes({std::make_pair(State::STANDBY, kStartCommand),
+                      std::make_pair(State::IDLE, kBurstCommand),
+                      std::make_pair(State::ACTIVE, kPauseCommand),
+                      std::make_pair(State::PAUSED, kFlushCommand)},
+                     State::STANDBY);
+    } else {
+        StateDag::Node last = d->makeFinalNode(State::IDLE);
+        StateDag::Node idle = d->makeNodes({std::make_pair(State::IDLE, kBurstCommand),
+                                            std::make_pair(State::ACTIVE, kPauseCommand),
+                                            std::make_pair(State::PAUSED, kFlushCommand)},
+                                           last);
+        if (!isSync) {
+            idle.children().push_back(
+                    d->makeNodes({std::make_pair(State::TRANSFERRING, kPauseCommand),
+                                  std::make_pair(State::TRANSFER_PAUSED, kFlushCommand)},
+                                 last));
+        }
+        d->makeNode(State::STANDBY, kStartCommand, idle);
+    }
+    return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kFlushInSeq = std::make_tuple(
         std::string("Flush"), 0, StreamTypeFilter::ANY, makeFlushCommands(true, false));
@@ -3374,13 +3587,21 @@
                         StreamTypeFilter::ASYNC, makeFlushCommands(false, false));
 
 std::shared_ptr<StateSequence> makeDrainPauseFlushOutCommands(bool isSync) {
-    return std::make_shared<SmartStateSequence>(std::vector<StateTransition>{
-            std::make_pair(kStartCommand, StreamDescriptor::State::IDLE),
-            std::make_pair(kBurstCommand, isSync ? StreamDescriptor::State::ACTIVE
-                                                 : StreamDescriptor::State::TRANSFERRING),
-            std::make_pair(kDrainOutAllCommand, StreamDescriptor::State::DRAINING),
-            std::make_pair(kPauseCommand, StreamDescriptor::State::DRAIN_PAUSED),
-            std::make_pair(kFlushCommand, StreamDescriptor::State::IDLE)});
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    StateDag::Node draining = d->makeNodes({std::make_pair(State::DRAINING, kPauseCommand),
+                                            std::make_pair(State::DRAIN_PAUSED, kFlushCommand)},
+                                           State::IDLE);
+    StateDag::Node active = d->makeNode(State::ACTIVE, kDrainOutAllCommand, draining);
+    StateDag::Node idle = d->makeNode(State::IDLE, kBurstCommand, active);
+    if (!isSync) {
+        idle.children().push_back(d->makeNode(State::TRANSFERRING, kDrainOutAllCommand, draining));
+    } else {
+        // If we get straight into IDLE on drain, no further testing is possible.
+        active.children().push_back(d->makeFinalNode(State::IDLE));
+    }
+    d->makeNode(State::STANDBY, kStartCommand, idle);
+    return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kDrainPauseFlushOutSyncSeq =
         std::make_tuple(std::string("DrainPauseFlush"), kStreamTransientStateTransitionDelayMs,