Merge "LE Audio Software offload: Handle death of client" into main
diff --git a/audio/aidl/common/include/Utils.h b/audio/aidl/common/include/Utils.h
index 52ae936..dc411ff 100644
--- a/audio/aidl/common/include/Utils.h
+++ b/audio/aidl/common/include/Utils.h
@@ -186,6 +186,12 @@
 
 template <typename E, typename U = std::underlying_type_t<E>,
           typename = std::enable_if_t<is_bit_position_enum<E>::value>>
+constexpr bool areAllBitPositionFlagsSet(U mask, std::initializer_list<E> flags) {
+    return (mask & makeBitPositionFlagMask<E>(flags)) == makeBitPositionFlagMask<E>(flags);
+}
+
+template <typename E, typename U = std::underlying_type_t<E>,
+          typename = std::enable_if_t<is_bit_position_enum<E>::value>>
 constexpr bool isAnyBitPositionFlagSet(U mask, std::initializer_list<E> flags) {
     return (mask & makeBitPositionFlagMask<E>(flags)) != 0;
 }
diff --git a/audio/aidl/default/Android.bp b/audio/aidl/default/Android.bp
index 73d7626..14082eb 100644
--- a/audio/aidl/default/Android.bp
+++ b/audio/aidl/default/Android.bp
@@ -77,8 +77,10 @@
         "r_submix/ModuleRemoteSubmix.cpp",
         "r_submix/SubmixRoute.cpp",
         "r_submix/StreamRemoteSubmix.cpp",
+        "stub/ApeHeader.cpp",
         "stub/DriverStubImpl.cpp",
         "stub/ModuleStub.cpp",
+        "stub/StreamOffloadStub.cpp",
         "stub/StreamStub.cpp",
         "usb/ModuleUsb.cpp",
         "usb/StreamUsb.cpp",
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index f9fa799..077d80b 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -211,9 +211,9 @@
         return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
     }
     const auto& flags = portConfigIt->flags.value();
-    StreamContext::DebugParameters params{
-            mDebug.streamTransientStateDelayMs, mVendorDebug.forceTransientBurst,
-            mVendorDebug.forceSynchronousDrain, mVendorDebug.forceDrainToDraining};
+    StreamContext::DebugParameters params{mDebug.streamTransientStateDelayMs,
+                                          mVendorDebug.forceTransientBurst,
+                                          mVendorDebug.forceSynchronousDrain};
     std::unique_ptr<StreamContext::DataMQ> dataMQ = nullptr;
     std::shared_ptr<IStreamCallback> streamAsyncCallback = nullptr;
     std::shared_ptr<ISoundDose> soundDose;
@@ -1546,7 +1546,6 @@
 
 const std::string Module::VendorDebug::kForceTransientBurstName = "aosp.forceTransientBurst";
 const std::string Module::VendorDebug::kForceSynchronousDrainName = "aosp.forceSynchronousDrain";
-const std::string Module::VendorDebug::kForceDrainToDrainingName = "aosp.forceDrainToDraining";
 
 ndk::ScopedAStatus Module::getVendorParameters(const std::vector<std::string>& in_ids,
                                                std::vector<VendorParameter>* _aidl_return) {
@@ -1561,10 +1560,6 @@
             VendorParameter forceSynchronousDrain{.id = id};
             forceSynchronousDrain.ext.setParcelable(Boolean{mVendorDebug.forceSynchronousDrain});
             _aidl_return->push_back(std::move(forceSynchronousDrain));
-        } else if (id == VendorDebug::kForceDrainToDrainingName) {
-            VendorParameter forceDrainToDraining{.id = id};
-            forceDrainToDraining.ext.setParcelable(Boolean{mVendorDebug.forceDrainToDraining});
-            _aidl_return->push_back(std::move(forceDrainToDraining));
         } else {
             allParametersKnown = false;
             LOG(VERBOSE) << __func__ << ": " << mType << ": unrecognized parameter \"" << id << "\"";
@@ -1605,10 +1600,6 @@
             if (!extractParameter<Boolean>(p, &mVendorDebug.forceSynchronousDrain)) {
                 return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
             }
-        } else if (p.id == VendorDebug::kForceDrainToDrainingName) {
-            if (!extractParameter<Boolean>(p, &mVendorDebug.forceDrainToDraining)) {
-                return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
-            }
         } else {
             allParametersKnown = false;
             LOG(VERBOSE) << __func__ << ": " << mType << ": unrecognized parameter \"" << p.id
diff --git a/audio/aidl/default/ModulePrimary.cpp b/audio/aidl/default/ModulePrimary.cpp
index 3da6d48..2a1dba9 100644
--- a/audio/aidl/default/ModulePrimary.cpp
+++ b/audio/aidl/default/ModulePrimary.cpp
@@ -21,12 +21,16 @@
 #include <android-base/logging.h>
 
 #include "core-impl/ModulePrimary.h"
+#include "core-impl/StreamOffloadStub.h"
 #include "core-impl/StreamPrimary.h"
 #include "core-impl/Telephony.h"
 
+using aidl::android::hardware::audio::common::areAllBitPositionFlagsSet;
 using aidl::android::hardware::audio::common::SinkMetadata;
 using aidl::android::hardware::audio::common::SourceMetadata;
+using aidl::android::media::audio::common::AudioIoFlags;
 using aidl::android::media::audio::common::AudioOffloadInfo;
+using aidl::android::media::audio::common::AudioOutputFlags;
 using aidl::android::media::audio::common::AudioPort;
 using aidl::android::media::audio::common::AudioPortConfig;
 using aidl::android::media::audio::common::MicrophoneInfo;
@@ -43,6 +47,17 @@
     return ndk::ScopedAStatus::ok();
 }
 
+ndk::ScopedAStatus ModulePrimary::calculateBufferSizeFrames(
+        const ::aidl::android::media::audio::common::AudioFormatDescription& format,
+        int32_t latencyMs, int32_t sampleRateHz, int32_t* bufferSizeFrames) {
+    if (format.type != ::aidl::android::media::audio::common::AudioFormatType::PCM &&
+        StreamOffloadStub::getSupportedEncodings().count(format.encoding)) {
+        *bufferSizeFrames = sampleRateHz / 2;  // 1/2 of a second.
+        return ndk::ScopedAStatus::ok();
+    }
+    return Module::calculateBufferSizeFrames(format, latencyMs, sampleRateHz, bufferSizeFrames);
+}
+
 ndk::ScopedAStatus ModulePrimary::createInputStream(StreamContext&& context,
                                                     const SinkMetadata& sinkMetadata,
                                                     const std::vector<MicrophoneInfo>& microphones,
@@ -54,8 +69,18 @@
 ndk::ScopedAStatus ModulePrimary::createOutputStream(
         StreamContext&& context, const SourceMetadata& sourceMetadata,
         const std::optional<AudioOffloadInfo>& offloadInfo, std::shared_ptr<StreamOut>* result) {
-    return createStreamInstance<StreamOutPrimary>(result, std::move(context), sourceMetadata,
-                                                  offloadInfo);
+    if (!areAllBitPositionFlagsSet(
+                context.getFlags().get<AudioIoFlags::output>(),
+                {AudioOutputFlags::COMPRESS_OFFLOAD, AudioOutputFlags::NON_BLOCKING})) {
+        return createStreamInstance<StreamOutPrimary>(result, std::move(context), sourceMetadata,
+                                                      offloadInfo);
+    } else {
+        // "Stub" is used because there is no actual decoder. The stream just
+        // extracts the clip duration from the media file header and simulates
+        // playback over time.
+        return createStreamInstance<StreamOutOffloadStub>(result, std::move(context),
+                                                          sourceMetadata, offloadInfo);
+    }
 }
 
 int32_t ModulePrimary::getNominalLatencyMs(const AudioPortConfig&) {
diff --git a/audio/aidl/default/Stream.cpp b/audio/aidl/default/Stream.cpp
index c138095..c6c1b5d 100644
--- a/audio/aidl/default/Stream.cpp
+++ b/audio/aidl/default/Stream.cpp
@@ -142,12 +142,16 @@
                    ", size in bytes: " + std::to_string(mDataBufferSize);
         }
     }
-    if (::android::status_t status = mDriver->init(); status != STATUS_OK) {
+    if (::android::status_t status = mDriver->init(this /*DriverCallbackInterface*/);
+        status != STATUS_OK) {
         return "Failed to initialize the driver: " + std::to_string(status);
     }
     return "";
 }
 
+void StreamWorkerCommonLogic::onBufferStateChange(size_t /*bufferFramesLeft*/) {}
+void StreamWorkerCommonLogic::onClipStateChange(size_t /*clipFramesLeft*/, bool /*hasNextClip*/) {}
+
 void StreamWorkerCommonLogic::populateReply(StreamDescriptor::Reply* reply,
                                             bool isConnected) const {
     static const StreamDescriptor::Position kUnknownPosition = {
@@ -381,48 +385,60 @@
 
 const std::string StreamOutWorkerLogic::kThreadName = "writer";
 
-StreamOutWorkerLogic::Status StreamOutWorkerLogic::cycle() {
-    if (mState == StreamDescriptor::State::DRAINING && mContext->getForceDrainToDraining() &&
-        mOnDrainReadyStatus == OnDrainReadyStatus::UNSENT) {
+void StreamOutWorkerLogic::onBufferStateChange(size_t bufferFramesLeft) {
+    const StreamDescriptor::State state = mState;
+    LOG(DEBUG) << __func__ << ": state: " << toString(state)
+               << ", bufferFramesLeft: " << bufferFramesLeft;
+    if (state == StreamDescriptor::State::TRANSFERRING) {
+        mState = StreamDescriptor::State::ACTIVE;
         std::shared_ptr<IStreamCallback> asyncCallback = mContext->getAsyncCallback();
         if (asyncCallback != nullptr) {
+            ndk::ScopedAStatus status = asyncCallback->onTransferReady();
+            if (!status.isOk()) {
+                LOG(ERROR) << __func__ << ": error from onTransferReady: " << status;
+            }
+        }
+    }
+}
+
+void StreamOutWorkerLogic::onClipStateChange(size_t clipFramesLeft, bool hasNextClip) {
+    const DrainState drainState = mDrainState;
+    std::shared_ptr<IStreamCallback> asyncCallback = mContext->getAsyncCallback();
+    LOG(DEBUG) << __func__ << ": drainState: " << drainState << "; clipFramesLeft "
+               << clipFramesLeft << "; hasNextClip? " << hasNextClip << "; asyncCallback? "
+               << (asyncCallback != nullptr);
+    if (drainState != DrainState::NONE && clipFramesLeft == 0) {
+        mState =
+                hasNextClip ? StreamDescriptor::State::TRANSFERRING : StreamDescriptor::State::IDLE;
+        mDrainState = DrainState::NONE;
+        if (drainState == DrainState::ALL && asyncCallback != nullptr) {
+            LOG(DEBUG) << __func__ << ": sending onDrainReady";
             ndk::ScopedAStatus status = asyncCallback->onDrainReady();
             if (!status.isOk()) {
                 LOG(ERROR) << __func__ << ": error from onDrainReady: " << status;
             }
-            // This sets the timeout for moving into IDLE on next iterations.
-            switchToTransientState(StreamDescriptor::State::DRAINING);
-            mOnDrainReadyStatus = OnDrainReadyStatus::SENT;
         }
-    } else if (mState == StreamDescriptor::State::DRAINING ||
-               mState == StreamDescriptor::State::TRANSFERRING) {
+    } else if (drainState == DrainState::EN && clipFramesLeft > 0) {
+        // The stream state does not change, it is still draining.
+        mDrainState = DrainState::EN_SENT;
+        if (asyncCallback != nullptr) {
+            LOG(DEBUG) << __func__ << ": sending onDrainReady";
+            ndk::ScopedAStatus status = asyncCallback->onDrainReady();
+            if (!status.isOk()) {
+                LOG(ERROR) << __func__ << ": error from onDrainReady: " << status;
+            }
+        }
+    }
+}
+
+StreamOutWorkerLogic::Status StreamOutWorkerLogic::cycle() {
+    // Non-blocking mode is handled within 'onClipStateChange'
+    if (std::shared_ptr<IStreamCallback> asyncCallback = mContext->getAsyncCallback();
+        mState == StreamDescriptor::State::DRAINING && asyncCallback == nullptr) {
         if (auto stateDurationMs = std::chrono::duration_cast<std::chrono::milliseconds>(
                     std::chrono::steady_clock::now() - mTransientStateStart);
             stateDurationMs >= mTransientStateDelayMs) {
-            std::shared_ptr<IStreamCallback> asyncCallback = mContext->getAsyncCallback();
-            if (asyncCallback == nullptr) {
-                // In blocking mode, mState can only be DRAINING.
-                mState = StreamDescriptor::State::IDLE;
-            } else {
-                // In a real implementation, the driver should notify the HAL about
-                // drain or transfer completion. In the stub, we switch unconditionally.
-                if (mState == StreamDescriptor::State::DRAINING) {
-                    mState = StreamDescriptor::State::IDLE;
-                    if (mOnDrainReadyStatus != OnDrainReadyStatus::SENT) {
-                        ndk::ScopedAStatus status = asyncCallback->onDrainReady();
-                        if (!status.isOk()) {
-                            LOG(ERROR) << __func__ << ": error from onDrainReady: " << status;
-                        }
-                        mOnDrainReadyStatus = OnDrainReadyStatus::SENT;
-                    }
-                } else {
-                    mState = StreamDescriptor::State::ACTIVE;
-                    ndk::ScopedAStatus status = asyncCallback->onTransferReady();
-                    if (!status.isOk()) {
-                        LOG(ERROR) << __func__ << ": error from onTransferReady: " << status;
-                    }
-                }
-            }
+            mState = StreamDescriptor::State::IDLE;
             if (mTransientStateDelayMs.count() != 0) {
                 LOG(DEBUG) << __func__ << ": switched to state " << toString(mState)
                            << " after a timeout";
@@ -552,10 +568,9 @@
                             mState = StreamDescriptor::State::IDLE;
                         } else {
                             switchToTransientState(StreamDescriptor::State::DRAINING);
-                            mOnDrainReadyStatus =
-                                    mode == StreamDescriptor::DrainMode::DRAIN_EARLY_NOTIFY
-                                            ? OnDrainReadyStatus::UNSENT
-                                            : OnDrainReadyStatus::IGNORE;
+                            mDrainState = mode == StreamDescriptor::DrainMode::DRAIN_EARLY_NOTIFY
+                                                  ? DrainState::EN
+                                                  : DrainState::ALL;
                         }
                     } else {
                         LOG(ERROR) << __func__ << ": drain failed: " << status;
diff --git a/audio/aidl/default/alsa/StreamAlsa.cpp b/audio/aidl/default/alsa/StreamAlsa.cpp
index 210c26b..7a44cc7 100644
--- a/audio/aidl/default/alsa/StreamAlsa.cpp
+++ b/audio/aidl/default/alsa/StreamAlsa.cpp
@@ -72,7 +72,7 @@
     return source;
 }
 
-::android::status_t StreamAlsa::init() {
+::android::status_t StreamAlsa::init(DriverCallbackInterface* /*callback*/) {
     return mConfig.has_value() ? ::android::OK : ::android::NO_INIT;
 }
 
diff --git a/audio/aidl/default/bluetooth/StreamBluetooth.cpp b/audio/aidl/default/bluetooth/StreamBluetooth.cpp
index 6e1a811..77ce121 100644
--- a/audio/aidl/default/bluetooth/StreamBluetooth.cpp
+++ b/audio/aidl/default/bluetooth/StreamBluetooth.cpp
@@ -70,7 +70,7 @@
     cleanupWorker();
 }
 
-::android::status_t StreamBluetooth::init() {
+::android::status_t StreamBluetooth::init(DriverCallbackInterface*) {
     std::lock_guard guard(mLock);
     if (mBtDeviceProxy == nullptr) {
         // This is a normal situation in VTS tests.
diff --git a/audio/aidl/default/include/core-impl/DriverStubImpl.h b/audio/aidl/default/include/core-impl/DriverStubImpl.h
index 40a9fea..a1a6c82 100644
--- a/audio/aidl/default/include/core-impl/DriverStubImpl.h
+++ b/audio/aidl/default/include/core-impl/DriverStubImpl.h
@@ -24,7 +24,7 @@
   public:
     explicit DriverStubImpl(const StreamContext& context);
 
-    ::android::status_t init() override;
+    ::android::status_t init(DriverCallbackInterface* callback) override;
     ::android::status_t drain(StreamDescriptor::DrainMode) override;
     ::android::status_t flush() override;
     ::android::status_t pause() override;
@@ -34,7 +34,7 @@
                                  int32_t* latencyMs) override;
     void shutdown() override;
 
-  private:
+  protected:
     const size_t mBufferSizeFrames;
     const size_t mFrameSizeBytes;
     const int mSampleRate;
diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h
index cbc13d1..6a43102 100644
--- a/audio/aidl/default/include/core-impl/Module.h
+++ b/audio/aidl/default/include/core-impl/Module.h
@@ -148,10 +148,8 @@
     struct VendorDebug {
         static const std::string kForceTransientBurstName;
         static const std::string kForceSynchronousDrainName;
-        static const std::string kForceDrainToDrainingName;
         bool forceTransientBurst = false;
         bool forceSynchronousDrain = false;
-        bool forceDrainToDraining = false;
     };
     // ids of device ports created at runtime via 'connectExternalDevice'.
     // Also stores a list of ids of mix ports with dynamic profiles that were populated from
diff --git a/audio/aidl/default/include/core-impl/ModulePrimary.h b/audio/aidl/default/include/core-impl/ModulePrimary.h
index 82c8a03..a657dc5 100644
--- a/audio/aidl/default/include/core-impl/ModulePrimary.h
+++ b/audio/aidl/default/include/core-impl/ModulePrimary.h
@@ -28,6 +28,9 @@
   protected:
     ndk::ScopedAStatus getTelephony(std::shared_ptr<ITelephony>* _aidl_return) override;
 
+    ndk::ScopedAStatus calculateBufferSizeFrames(
+            const ::aidl::android::media::audio::common::AudioFormatDescription& format,
+            int32_t latencyMs, int32_t sampleRateHz, int32_t* bufferSizeFrames) override;
     ndk::ScopedAStatus createInputStream(
             StreamContext&& context,
             const ::aidl::android::hardware::audio::common::SinkMetadata& sinkMetadata,
diff --git a/audio/aidl/default/include/core-impl/Stream.h b/audio/aidl/default/include/core-impl/Stream.h
index d8bac82..f0139b4 100644
--- a/audio/aidl/default/include/core-impl/Stream.h
+++ b/audio/aidl/default/include/core-impl/Stream.h
@@ -78,10 +78,6 @@
         bool forceTransientBurst = false;
         // Force the "drain" command to be synchronous, going directly to the IDLE state.
         bool forceSynchronousDrain = false;
-        // Force the "drain early notify" command to keep the SM in the DRAINING state
-        // after sending 'onDrainReady' callback. The SM moves to IDLE after
-        // 'transientStateDelayMs'.
-        bool forceDrainToDraining = false;
     };
 
     StreamContext() = default;
@@ -123,7 +119,6 @@
     ::aidl::android::media::audio::common::AudioIoFlags getFlags() const { return mFlags; }
     bool getForceTransientBurst() const { return mDebugParameters.forceTransientBurst; }
     bool getForceSynchronousDrain() const { return mDebugParameters.forceSynchronousDrain; }
-    bool getForceDrainToDraining() const { return mDebugParameters.forceDrainToDraining; }
     size_t getFrameSize() const;
     int getInternalCommandCookie() const { return mInternalCommandCookie; }
     int32_t getMixPortHandle() const { return mMixPortHandle; }
@@ -168,11 +163,27 @@
     int64_t mFrameCount = 0;
 };
 
+// Driver callbacks are executed on a dedicated thread, not on the worker thread.
+struct DriverCallbackInterface {
+    virtual ~DriverCallbackInterface() = default;
+    // Both callbacks are used to notify the worker about the progress of the playback
+    // offloaded to the DSP.
+
+    //   'bufferFramesLeft' is how many *encoded* frames are left in the buffer until
+    //    it depletes.
+    virtual void onBufferStateChange(size_t bufferFramesLeft) = 0;
+    //   'clipFramesLeft' is how many *decoded* frames are left until the end of the currently
+    //    playing clip. '0' frames left means that the clip has ended (by itself or due
+    //    to draining).
+    //   'hasNextClip' indicates whether the DSP has audio data for the next clip.
+    virtual void onClipStateChange(size_t clipFramesLeft, bool hasNextClip) = 0;
+};
+
 // This interface provides operations of the stream which are executed on the worker thread.
 struct DriverInterface {
     virtual ~DriverInterface() = default;
     // All the methods below are called on the worker thread.
-    virtual ::android::status_t init() = 0;  // This function is only called once.
+    virtual ::android::status_t init(DriverCallbackInterface* callback) = 0;  // Called once.
     virtual ::android::status_t drain(StreamDescriptor::DrainMode mode) = 0;
     virtual ::android::status_t flush() = 0;
     virtual ::android::status_t pause() = 0;
@@ -194,7 +205,8 @@
     virtual void shutdown() = 0;  // This function is only called once.
 };
 
-class StreamWorkerCommonLogic : public ::android::hardware::audio::common::StreamLogic {
+class StreamWorkerCommonLogic : public ::android::hardware::audio::common::StreamLogic,
+                                public DriverCallbackInterface {
   public:
     bool isClosed() const { return mState == StreamContext::STATE_CLOSED; }
     StreamDescriptor::State setClosed() {
@@ -214,7 +226,13 @@
           mDriver(driver),
           mTransientStateDelayMs(context->getTransientStateDelayMs()) {}
     pid_t getTid() const;
+
+    // ::android::hardware::audio::common::StreamLogic
     std::string init() override;
+    // DriverCallbackInterface
+    void onBufferStateChange(size_t bufferFramesLeft) override;
+    void onClipStateChange(size_t clipFramesLeft, bool hasNextClip) override;
+
     void populateReply(StreamDescriptor::Reply* reply, bool isConnected) const;
     void populateReplyWrongState(StreamDescriptor::Reply* reply,
                                  const StreamDescriptor::Command& command) const;
@@ -301,14 +319,17 @@
 
   protected:
     Status cycle() override;
+    // DriverCallbackInterface
+    void onBufferStateChange(size_t bufferFramesLeft) override;
+    void onClipStateChange(size_t clipFramesLeft, bool hasNextClip) override;
 
   private:
     bool write(size_t clientSize, StreamDescriptor::Reply* reply);
 
     std::shared_ptr<IStreamOutEventCallback> mEventCallback;
 
-    enum OnDrainReadyStatus : int32_t { IGNORE /*used for DRAIN_ALL*/, UNSENT, SENT };
-    OnDrainReadyStatus mOnDrainReadyStatus = OnDrainReadyStatus::IGNORE;
+    enum DrainState : int32_t { NONE, ALL, EN /*early notify*/, EN_SENT };
+    std::atomic<DrainState> mDrainState = DrainState::NONE;
 };
 using StreamOutWorker = StreamWorkerImpl<StreamOutWorkerLogic>;
 
diff --git a/audio/aidl/default/include/core-impl/StreamAlsa.h b/audio/aidl/default/include/core-impl/StreamAlsa.h
index 7e0f0ac..c0dcb63 100644
--- a/audio/aidl/default/include/core-impl/StreamAlsa.h
+++ b/audio/aidl/default/include/core-impl/StreamAlsa.h
@@ -40,7 +40,7 @@
     ~StreamAlsa();
 
     // Methods of 'DriverInterface'.
-    ::android::status_t init() override;
+    ::android::status_t init(DriverCallbackInterface* callback) override;
     ::android::status_t drain(StreamDescriptor::DrainMode) override;
     ::android::status_t flush() override;
     ::android::status_t pause() override;
diff --git a/audio/aidl/default/include/core-impl/StreamBluetooth.h b/audio/aidl/default/include/core-impl/StreamBluetooth.h
index 357a546..2bdd6b2 100644
--- a/audio/aidl/default/include/core-impl/StreamBluetooth.h
+++ b/audio/aidl/default/include/core-impl/StreamBluetooth.h
@@ -44,7 +44,7 @@
     ~StreamBluetooth();
 
     // Methods of 'DriverInterface'.
-    ::android::status_t init() override;
+    ::android::status_t init(DriverCallbackInterface*) override;
     ::android::status_t drain(StreamDescriptor::DrainMode) override;
     ::android::status_t flush() override;
     ::android::status_t pause() override;
diff --git a/audio/aidl/default/include/core-impl/StreamOffloadStub.h b/audio/aidl/default/include/core-impl/StreamOffloadStub.h
new file mode 100644
index 0000000..3b452f9
--- /dev/null
+++ b/audio/aidl/default/include/core-impl/StreamOffloadStub.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+#pragma once
+
+#include <mutex>
+#include <set>
+#include <string>
+
+#include "core-impl/DriverStubImpl.h"
+#include "core-impl/Stream.h"
+
+namespace aidl::android::hardware::audio::core {
+
+struct DspSimulatorState {
+    const std::string formatEncoding;
+    const int sampleRate;
+    const int64_t earlyNotifyFrames;
+    const int64_t bufferNotifyFrames;
+    DriverCallbackInterface* callback = nullptr;  // set before starting DSP worker
+    std::mutex lock;
+    std::vector<int64_t> clipFramesLeft GUARDED_BY(lock);
+    int64_t bufferFramesLeft GUARDED_BY(lock);
+};
+
+class DspSimulatorLogic : public ::android::hardware::audio::common::StreamLogic {
+  protected:
+    explicit DspSimulatorLogic(DspSimulatorState& sharedState) : mSharedState(sharedState) {}
+    std::string init() override;
+    Status cycle() override;
+
+  private:
+    DspSimulatorState& mSharedState;
+};
+
+class DspSimulatorWorker
+    : public ::android::hardware::audio::common::StreamWorker<DspSimulatorLogic> {
+  public:
+    explicit DspSimulatorWorker(DspSimulatorState& sharedState)
+        : ::android::hardware::audio::common::StreamWorker<DspSimulatorLogic>(sharedState) {}
+};
+
+class DriverOffloadStubImpl : public DriverStubImpl {
+  public:
+    DriverOffloadStubImpl(const StreamContext& context);
+    ::android::status_t init(DriverCallbackInterface* callback) override;
+    ::android::status_t drain(StreamDescriptor::DrainMode drainMode) override;
+    ::android::status_t flush() override;
+    ::android::status_t pause() override;
+    ::android::status_t transfer(void* buffer, size_t frameCount, size_t* actualFrameCount,
+                                 int32_t* latencyMs) override;
+    void shutdown() override;
+
+  private:
+    DspSimulatorState mState;
+    DspSimulatorWorker mDspWorker;
+    bool mDspWorkerStarted = false;
+};
+
+class StreamOffloadStub : public StreamCommonImpl, public DriverOffloadStubImpl {
+  public:
+    static const std::set<std::string>& getSupportedEncodings();
+
+    StreamOffloadStub(StreamContext* context, const Metadata& metadata);
+    ~StreamOffloadStub();
+};
+
+class StreamOutOffloadStub final : public StreamOut, public StreamOffloadStub {
+  public:
+    friend class ndk::SharedRefBase;
+    StreamOutOffloadStub(
+            StreamContext&& context,
+            const ::aidl::android::hardware::audio::common::SourceMetadata& sourceMetadata,
+            const std::optional<::aidl::android::media::audio::common::AudioOffloadInfo>&
+                    offloadInfo);
+
+  private:
+    void onClose(StreamDescriptor::State) override { defaultOnClose(); }
+};
+
+}  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/include/core-impl/StreamPrimary.h b/audio/aidl/default/include/core-impl/StreamPrimary.h
index 4f19a46..06f8bc3 100644
--- a/audio/aidl/default/include/core-impl/StreamPrimary.h
+++ b/audio/aidl/default/include/core-impl/StreamPrimary.h
@@ -32,7 +32,7 @@
     StreamPrimary(StreamContext* context, const Metadata& metadata);
 
     // Methods of 'DriverInterface'.
-    ::android::status_t init() override;
+    ::android::status_t init(DriverCallbackInterface* callback) override;
     ::android::status_t drain(StreamDescriptor::DrainMode mode) override;
     ::android::status_t flush() override;
     ::android::status_t pause() override;
diff --git a/audio/aidl/default/include/core-impl/StreamRemoteSubmix.h b/audio/aidl/default/include/core-impl/StreamRemoteSubmix.h
index 5e52ad0..28a446a 100644
--- a/audio/aidl/default/include/core-impl/StreamRemoteSubmix.h
+++ b/audio/aidl/default/include/core-impl/StreamRemoteSubmix.h
@@ -32,7 +32,7 @@
     ~StreamRemoteSubmix();
 
     // Methods of 'DriverInterface'.
-    ::android::status_t init() override;
+    ::android::status_t init(DriverCallbackInterface*) override;
     ::android::status_t drain(StreamDescriptor::DrainMode) override;
     ::android::status_t flush() override;
     ::android::status_t pause() override;
diff --git a/audio/aidl/default/primary/StreamPrimary.cpp b/audio/aidl/default/primary/StreamPrimary.cpp
index 46e384e..8455680 100644
--- a/audio/aidl/default/primary/StreamPrimary.cpp
+++ b/audio/aidl/default/primary/StreamPrimary.cpp
@@ -46,9 +46,9 @@
     context->startStreamDataProcessor();
 }
 
-::android::status_t StreamPrimary::init() {
-    RETURN_STATUS_IF_ERROR(mStubDriver.init());
-    return StreamAlsa::init();
+::android::status_t StreamPrimary::init(DriverCallbackInterface* callback) {
+    RETURN_STATUS_IF_ERROR(mStubDriver.init(callback));
+    return StreamAlsa::init(callback);
 }
 
 ::android::status_t StreamPrimary::drain(StreamDescriptor::DrainMode mode) {
diff --git a/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp b/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp
index f8ead16..cc3c644 100644
--- a/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp
+++ b/audio/aidl/default/r_submix/StreamRemoteSubmix.cpp
@@ -51,7 +51,7 @@
     cleanupWorker();
 }
 
-::android::status_t StreamRemoteSubmix::init() {
+::android::status_t StreamRemoteSubmix::init(DriverCallbackInterface*) {
     mCurrentRoute = SubmixRoute::findOrCreateRoute(mDeviceAddress, mStreamConfig);
     if (mCurrentRoute == nullptr) {
         return ::android::NO_INIT;
diff --git a/audio/aidl/default/stub/ApeHeader.cpp b/audio/aidl/default/stub/ApeHeader.cpp
new file mode 100644
index 0000000..9112377
--- /dev/null
+++ b/audio/aidl/default/stub/ApeHeader.cpp
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+#define LOG_TAG "AHAL_OffloadStream"
+#include <android-base/logging.h>
+
+#include "ApeHeader.h"
+
+namespace aidl::android::hardware::audio::core {
+
+static constexpr uint32_t kApeSignature1 = 0x2043414d;  // 'MAC ';
+static constexpr uint32_t kApeSignature2 = 0x4643414d;  // 'MACF';
+static constexpr uint16_t kMinimumVersion = 3980;
+
+void* findApeHeader(void* buffer, size_t bufferSizeBytes, ApeHeader** header) {
+    auto advanceBy = [&](size_t bytes) -> void* {
+        buffer = static_cast<uint8_t*>(buffer) + bytes;
+        bufferSizeBytes -= bytes;
+        return buffer;
+    };
+
+    while (bufferSizeBytes >= sizeof(ApeDescriptor) + sizeof(ApeHeader)) {
+        ApeDescriptor* descPtr = static_cast<ApeDescriptor*>(buffer);
+        if (descPtr->signature != kApeSignature1 && descPtr->signature != kApeSignature2) {
+            advanceBy(sizeof(descPtr->signature));
+            continue;
+        }
+        if (descPtr->version < kMinimumVersion) {
+            LOG(ERROR) << __func__ << ": Unsupported APE version: " << descPtr->version
+                       << ", minimum supported version: " << kMinimumVersion;
+            // Older versions only have a header, which is of the size similar to the modern header.
+            advanceBy(sizeof(ApeHeader));
+            continue;
+        }
+        if (descPtr->descriptorSizeBytes > bufferSizeBytes) {
+            LOG(ERROR) << __func__
+                       << ": Invalid APE descriptor size: " << descPtr->descriptorSizeBytes
+                       << ", overruns remaining buffer size: " << bufferSizeBytes;
+            advanceBy(sizeof(ApeDescriptor));
+            continue;
+        }
+        advanceBy(descPtr->descriptorSizeBytes);
+        if (sizeof(ApeHeader) > bufferSizeBytes) {
+            LOG(ERROR) << __func__ << ": APE header is incomplete, want: " << sizeof(ApeHeader)
+                       << " bytes, have: " << bufferSizeBytes;
+            return nullptr;
+        }
+        *header = static_cast<ApeHeader*>(buffer);
+        return advanceBy(sizeof(ApeHeader));
+    }
+    return nullptr;
+}
+
+}  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/stub/ApeHeader.h b/audio/aidl/default/stub/ApeHeader.h
new file mode 100644
index 0000000..df30335
--- /dev/null
+++ b/audio/aidl/default/stub/ApeHeader.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+
+namespace aidl::android::hardware::audio::core {
+
+// Simplified APE (Monkey Audio) header definition sufficient to figure out
+// the basic parameters of the encoded file. Only supports the "current"
+// versions of the header (>= 3980).
+
+#pragma pack(push, 4)
+
+// Only the beginning of the descriptor is needed to find the header which
+// follows the descriptor.
+struct ApeDescriptor {
+    uint32_t signature;  // 'MAC ' or 'MACF'
+    uint16_t version;
+    uint16_t padding;
+    uint32_t descriptorSizeBytes;
+    uint32_t headerSizeBytes;
+};
+
+struct ApeHeader {
+    uint16_t compressionLevel;
+    uint16_t flags;
+    uint32_t blocksPerFrame;   // "frames" are encoder frames, while "blocks" are audio frames
+    uint32_t lastFrameBlocks;  // number of "blocks" in the last encoder "frame"
+    uint32_t totalFrames;      // total number of encoder "frames"
+    uint16_t bitsPerSample;
+    uint16_t channelCount;
+    uint32_t sampleRate;
+};
+
+#pragma pack(pop)
+
+// Tries to find APE descriptor and header in the buffer. Returns the position
+// after the header or nullptr if it was not found.
+void* findApeHeader(void* buffer, size_t bufferSizeBytes, ApeHeader** header);
+
+// Clip duration in audio frames ("blocks" in the APE terminology).
+inline int64_t getApeClipDurationFrames(const ApeHeader* header) {
+    return header->totalFrames != 0
+                   ? (header->totalFrames - 1) * header->blocksPerFrame + header->lastFrameBlocks
+                   : 0;
+}
+
+}  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/stub/DriverStubImpl.cpp b/audio/aidl/default/stub/DriverStubImpl.cpp
index beb0114..107affb 100644
--- a/audio/aidl/default/stub/DriverStubImpl.cpp
+++ b/audio/aidl/default/stub/DriverStubImpl.cpp
@@ -31,7 +31,7 @@
       mIsAsynchronous(!!context.getAsyncCallback()),
       mIsInput(context.isInput()) {}
 
-::android::status_t DriverStubImpl::init() {
+::android::status_t DriverStubImpl::init(DriverCallbackInterface* /*callback*/) {
     mIsInitialized = true;
     return ::android::OK;
 }
diff --git a/audio/aidl/default/stub/StreamOffloadStub.cpp b/audio/aidl/default/stub/StreamOffloadStub.cpp
new file mode 100644
index 0000000..fb12697
--- /dev/null
+++ b/audio/aidl/default/stub/StreamOffloadStub.cpp
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+#define LOG_TAG "AHAL_OffloadStream"
+#include <android-base/logging.h>
+#include <audio_utils/clock.h>
+#include <error/Result.h>
+#include <utils/SystemClock.h>
+
+#include "ApeHeader.h"
+#include "core-impl/StreamOffloadStub.h"
+
+using aidl::android::hardware::audio::common::SourceMetadata;
+using aidl::android::media::audio::common::AudioDevice;
+using aidl::android::media::audio::common::AudioOffloadInfo;
+using aidl::android::media::audio::common::MicrophoneInfo;
+
+namespace aidl::android::hardware::audio::core {
+
+std::string DspSimulatorLogic::init() {
+    return "";
+}
+
+DspSimulatorLogic::Status DspSimulatorLogic::cycle() {
+    std::vector<std::pair<int64_t, bool>> clipNotifies;
+    // Simulate playback.
+    const int64_t timeBeginNs = ::android::uptimeNanos();
+    usleep(1000);
+    const int64_t clipFramesPlayed =
+            (::android::uptimeNanos() - timeBeginNs) * mSharedState.sampleRate / NANOS_PER_SECOND;
+    const int64_t bufferFramesConsumed = clipFramesPlayed / 2;  // assume 1:2 compression ratio
+    int64_t bufferFramesLeft = 0;
+    {
+        std::lock_guard l(mSharedState.lock);
+        mSharedState.bufferFramesLeft =
+                mSharedState.bufferFramesLeft > bufferFramesConsumed
+                        ? mSharedState.bufferFramesLeft - bufferFramesConsumed
+                        : 0;
+        bufferFramesLeft = mSharedState.bufferFramesLeft;
+        int64_t framesPlayed = clipFramesPlayed;
+        while (framesPlayed > 0 && !mSharedState.clipFramesLeft.empty()) {
+            LOG(VERBOSE) << __func__ << ": clips: "
+                         << ::android::internal::ToString(mSharedState.clipFramesLeft);
+            const bool hasNextClip = mSharedState.clipFramesLeft.size() > 1;
+            if (mSharedState.clipFramesLeft[0] > framesPlayed) {
+                mSharedState.clipFramesLeft[0] -= framesPlayed;
+                framesPlayed = 0;
+                if (mSharedState.clipFramesLeft[0] <= mSharedState.earlyNotifyFrames) {
+                    clipNotifies.emplace_back(mSharedState.clipFramesLeft[0], hasNextClip);
+                }
+            } else {
+                clipNotifies.emplace_back(0 /*clipFramesLeft*/, hasNextClip);
+                framesPlayed -= mSharedState.clipFramesLeft[0];
+                mSharedState.clipFramesLeft.erase(mSharedState.clipFramesLeft.begin());
+            }
+        }
+    }
+    if (bufferFramesLeft <= mSharedState.bufferNotifyFrames) {
+        LOG(DEBUG) << __func__ << ": sending onBufferStateChange: " << bufferFramesLeft;
+        mSharedState.callback->onBufferStateChange(bufferFramesLeft);
+    }
+    for (const auto& notify : clipNotifies) {
+        LOG(DEBUG) << __func__ << ": sending onClipStateChange: " << notify.first << ", "
+                   << notify.second;
+        mSharedState.callback->onClipStateChange(notify.first, notify.second);
+    }
+    return Status::CONTINUE;
+}
+
+DriverOffloadStubImpl::DriverOffloadStubImpl(const StreamContext& context)
+    : DriverStubImpl(context),
+      mState{context.getFormat().encoding, context.getSampleRate(),
+             250 /*earlyNotifyMs*/ * context.getSampleRate() / MILLIS_PER_SECOND,
+             static_cast<int64_t>(context.getBufferSizeInFrames()) / 2},
+      mDspWorker(mState) {}
+
+::android::status_t DriverOffloadStubImpl::init(DriverCallbackInterface* callback) {
+    RETURN_STATUS_IF_ERROR(DriverStubImpl::init(callback));
+    if (!StreamOffloadStub::getSupportedEncodings().count(mState.formatEncoding)) {
+        LOG(ERROR) << __func__ << ": encoded format \"" << mState.formatEncoding
+                   << "\" is not supported";
+        return ::android::NO_INIT;
+    }
+    mState.callback = callback;
+    return ::android::OK;
+}
+
+::android::status_t DriverOffloadStubImpl::drain(StreamDescriptor::DrainMode drainMode) {
+    // Does not call into the DriverStubImpl::drain.
+    if (!mIsInitialized) {
+        LOG(FATAL) << __func__ << ": must not happen for an uninitialized driver";
+    }
+    std::lock_guard l(mState.lock);
+    if (!mState.clipFramesLeft.empty()) {
+        // Cut playback of the current clip.
+        mState.clipFramesLeft[0] = std::min(mState.earlyNotifyFrames * 2, mState.clipFramesLeft[0]);
+        if (drainMode == StreamDescriptor::DrainMode::DRAIN_ALL) {
+            // Make sure there are no clips after the current one.
+            mState.clipFramesLeft.resize(1);
+        }
+    }
+    return ::android::OK;
+}
+
+::android::status_t DriverOffloadStubImpl::flush() {
+    RETURN_STATUS_IF_ERROR(DriverStubImpl::flush());
+    mDspWorker.pause();
+    {
+        std::lock_guard l(mState.lock);
+        mState.clipFramesLeft.clear();
+        mState.bufferFramesLeft = 0;
+    }
+    return ::android::OK;
+}
+
+::android::status_t DriverOffloadStubImpl::pause() {
+    RETURN_STATUS_IF_ERROR(DriverStubImpl::pause());
+    mDspWorker.pause();
+    return ::android::OK;
+}
+
+::android::status_t DriverOffloadStubImpl::transfer(void* buffer, size_t frameCount,
+                                                    size_t* actualFrameCount,
+                                                    int32_t* /*latencyMs*/) {
+    // Does not call into the DriverStubImpl::transfer.
+    if (!mIsInitialized) {
+        LOG(FATAL) << __func__ << ": must not happen for an uninitialized driver";
+    }
+    if (mIsStandby) {
+        LOG(FATAL) << __func__ << ": must not happen while in standby";
+    }
+    if (!mDspWorkerStarted) {
+        // This is an "audio service thread," must have elevated priority.
+        if (!mDspWorker.start("dsp_sim", ANDROID_PRIORITY_URGENT_AUDIO)) {
+            return ::android::NO_INIT;
+        }
+        mDspWorkerStarted = true;
+    }
+    // Scan the buffer for clip headers.
+    *actualFrameCount = frameCount;
+    while (buffer != nullptr && frameCount > 0) {
+        ApeHeader* apeHeader = nullptr;
+        void* prevBuffer = buffer;
+        buffer = findApeHeader(prevBuffer, frameCount * mFrameSizeBytes, &apeHeader);
+        if (buffer != nullptr && apeHeader != nullptr) {
+            // Frame count does not include the size of the header data.
+            const size_t headerSizeFrames =
+                    (static_cast<uint8_t*>(buffer) - static_cast<uint8_t*>(prevBuffer)) /
+                    mFrameSizeBytes;
+            frameCount -= headerSizeFrames;
+            *actualFrameCount = frameCount;
+            // Stage the clip duration into the DSP worker's queue.
+            const int64_t clipDurationFrames = getApeClipDurationFrames(apeHeader);
+            const int32_t clipSampleRate = apeHeader->sampleRate;
+            LOG(DEBUG) << __func__ << ": found APE clip of " << clipDurationFrames << " frames, "
+                       << "sample rate: " << clipSampleRate;
+            if (clipSampleRate == mState.sampleRate) {
+                std::lock_guard l(mState.lock);
+                mState.clipFramesLeft.push_back(clipDurationFrames);
+            } else {
+                LOG(ERROR) << __func__ << ": clip sample rate " << clipSampleRate
+                           << " does not match stream sample rate " << mState.sampleRate;
+            }
+        } else {
+            frameCount = 0;
+        }
+    }
+    {
+        std::lock_guard l(mState.lock);
+        mState.bufferFramesLeft = *actualFrameCount;
+    }
+    mDspWorker.resume();
+    return ::android::OK;
+}
+
+void DriverOffloadStubImpl::shutdown() {
+    LOG(DEBUG) << __func__ << ": stopping the DSP simulator worker";
+    mDspWorker.stop();
+}
+
+// static
+const std::set<std::string>& StreamOffloadStub::getSupportedEncodings() {
+    static const std::set<std::string> kSupportedEncodings = {
+            "audio/x-ape",
+    };
+    return kSupportedEncodings;
+}
+
+StreamOffloadStub::StreamOffloadStub(StreamContext* context, const Metadata& metadata)
+    : StreamCommonImpl(context, metadata), DriverOffloadStubImpl(getContext()) {}
+
+StreamOffloadStub::~StreamOffloadStub() {
+    cleanupWorker();
+}
+
+StreamOutOffloadStub::StreamOutOffloadStub(StreamContext&& context,
+                                           const SourceMetadata& sourceMetadata,
+                                           const std::optional<AudioOffloadInfo>& offloadInfo)
+    : StreamOut(std::move(context), offloadInfo),
+      StreamOffloadStub(&mContextInstance, sourceMetadata) {}
+
+}  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/stub/StreamStub.cpp b/audio/aidl/default/stub/StreamStub.cpp
index f6c87e1..2278880 100644
--- a/audio/aidl/default/stub/StreamStub.cpp
+++ b/audio/aidl/default/stub/StreamStub.cpp
@@ -14,11 +14,8 @@
  * limitations under the License.
  */
 
-#include <cmath>
-
 #define LOG_TAG "AHAL_Stream"
 #include <android-base/logging.h>
-#include <audio_utils/clock.h>
 
 #include "core-impl/Module.h"
 #include "core-impl/StreamStub.h"
diff --git a/audio/aidl/vts/Android.bp b/audio/aidl/vts/Android.bp
index 14e70ef..f855038 100644
--- a/audio/aidl/vts/Android.bp
+++ b/audio/aidl/vts/Android.bp
@@ -41,7 +41,6 @@
         "-Wthread-safety",
         "-Wno-error=unused-parameter",
     ],
-    test_config_template: "VtsHalAudioTargetTestTemplate.xml",
     test_suites: [
         "general-tests",
         "vts",
@@ -60,6 +59,7 @@
     srcs: [
         ":effectCommonFile",
     ],
+    test_config_template: "VtsHalAudioEffectTargetTestTemplate.xml",
 }
 
 cc_test {
@@ -77,6 +77,11 @@
         "VtsHalAudioCoreConfigTargetTest.cpp",
         "VtsHalAudioCoreModuleTargetTest.cpp",
     ],
+    data: [
+        "data/sine882hz_44100_3s.ape",
+        "data/sine960hz_48000_3s.ape",
+    ],
+    test_config_template: "VtsHalAudioCoreTargetTestTemplate.xml",
 }
 
 cc_test {
diff --git a/audio/aidl/vts/ModuleConfig.cpp b/audio/aidl/vts/ModuleConfig.cpp
index d24c4c8..7d4cc70 100644
--- a/audio/aidl/vts/ModuleConfig.cpp
+++ b/audio/aidl/vts/ModuleConfig.cpp
@@ -36,12 +36,10 @@
 using aidl::android::media::audio::common::AudioChannelLayout;
 using aidl::android::media::audio::common::AudioDeviceDescription;
 using aidl::android::media::audio::common::AudioDeviceType;
-using aidl::android::media::audio::common::AudioEncapsulationMode;
 using aidl::android::media::audio::common::AudioFormatDescription;
 using aidl::android::media::audio::common::AudioFormatType;
 using aidl::android::media::audio::common::AudioInputFlags;
 using aidl::android::media::audio::common::AudioIoFlags;
-using aidl::android::media::audio::common::AudioOffloadInfo;
 using aidl::android::media::audio::common::AudioOutputFlags;
 using aidl::android::media::audio::common::AudioPort;
 using aidl::android::media::audio::common::AudioPortConfig;
@@ -51,26 +49,6 @@
 using aidl::android::media::audio::common::Int;
 
 // static
-std::optional<AudioOffloadInfo> ModuleConfig::generateOffloadInfoIfNeeded(
-        const AudioPortConfig& portConfig) {
-    if (portConfig.flags.has_value() &&
-        portConfig.flags.value().getTag() == AudioIoFlags::Tag::output &&
-        isBitPositionFlagSet(portConfig.flags.value().get<AudioIoFlags::Tag::output>(),
-                             AudioOutputFlags::COMPRESS_OFFLOAD)) {
-        AudioOffloadInfo offloadInfo;
-        offloadInfo.base.sampleRate = portConfig.sampleRate.value().value;
-        offloadInfo.base.channelMask = portConfig.channelMask.value();
-        offloadInfo.base.format = portConfig.format.value();
-        offloadInfo.bitRatePerSecond = 256000;                             // Arbitrary value.
-        offloadInfo.durationUs = std::chrono::microseconds(1min).count();  // Arbitrary value.
-        offloadInfo.usage = AudioUsage::MEDIA;
-        offloadInfo.encapsulationMode = AudioEncapsulationMode::NONE;
-        return offloadInfo;
-    }
-    return {};
-}
-
-// static
 std::vector<aidl::android::media::audio::common::AudioPort>
 ModuleConfig::getAudioPortsForDeviceTypes(
         const std::vector<aidl::android::media::audio::common::AudioPort>& ports,
diff --git a/audio/aidl/vts/ModuleConfig.h b/audio/aidl/vts/ModuleConfig.h
index 27286e5..d45ccda 100644
--- a/audio/aidl/vts/ModuleConfig.h
+++ b/audio/aidl/vts/ModuleConfig.h
@@ -34,10 +34,6 @@
     using SrcSinkGroup =
             std::pair<aidl::android::hardware::audio::core::AudioRoute, std::vector<SrcSinkPair>>;
 
-    static std::optional<aidl::android::media::audio::common::AudioOffloadInfo>
-    generateOffloadInfoIfNeeded(
-            const aidl::android::media::audio::common::AudioPortConfig& portConfig);
-
     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,
diff --git a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
index 750e54d..8bbb60b 100644
--- a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
@@ -19,6 +19,7 @@
 #include <cmath>
 #include <condition_variable>
 #include <forward_list>
+#include <fstream>
 #include <limits>
 #include <memory>
 #include <mutex>
@@ -81,12 +82,15 @@
 using aidl::android::hardware::audio::core::sounddose::ISoundDose;
 using aidl::android::hardware::common::fmq::SynchronizedReadWrite;
 using aidl::android::media::audio::common::AudioChannelLayout;
+using aidl::android::media::audio::common::AudioConfigBase;
 using aidl::android::media::audio::common::AudioContentType;
 using aidl::android::media::audio::common::AudioDevice;
 using aidl::android::media::audio::common::AudioDeviceAddress;
 using aidl::android::media::audio::common::AudioDeviceDescription;
 using aidl::android::media::audio::common::AudioDeviceType;
 using aidl::android::media::audio::common::AudioDualMonoMode;
+using aidl::android::media::audio::common::AudioEncapsulationMode;
+using aidl::android::media::audio::common::AudioFormatDescription;
 using aidl::android::media::audio::common::AudioFormatType;
 using aidl::android::media::audio::common::AudioGainConfig;
 using aidl::android::media::audio::common::AudioInputFlags;
@@ -96,6 +100,7 @@
 using aidl::android::media::audio::common::AudioMMapPolicyInfo;
 using aidl::android::media::audio::common::AudioMMapPolicyType;
 using aidl::android::media::audio::common::AudioMode;
+using aidl::android::media::audio::common::AudioOffloadInfo;
 using aidl::android::media::audio::common::AudioOutputFlags;
 using aidl::android::media::audio::common::AudioPlaybackRate;
 using aidl::android::media::audio::common::AudioPort;
@@ -217,6 +222,59 @@
     return result;
 }
 
+static const AudioFormatDescription kApeFileAudioFormat = {.encoding = "audio/x-ape"};
+static const AudioChannelLayout kApeFileChannelMask =
+        AudioChannelLayout::make<AudioChannelLayout::layoutMask>(AudioChannelLayout::LAYOUT_MONO);
+struct MediaFileInfo {
+    std::string path;
+    int32_t bps;
+    int32_t durationMs;
+};
+static const std::map<AudioConfigBase, MediaFileInfo> kMediaFileDataInfos = {
+        {{44100, kApeFileChannelMask, kApeFileAudioFormat},
+         {"/data/local/tmp/sine882hz_44100_3s.ape", 217704, 3000}},
+        {{48000, kApeFileChannelMask, kApeFileAudioFormat},
+         {"/data/local/tmp/sine960hz_48000_3s.ape", 236256, 3000}},
+};
+
+std::optional<MediaFileInfo> getMediaFileInfoForConfig(const AudioConfigBase& config) {
+    const auto it = kMediaFileDataInfos.find(config);
+    if (it != kMediaFileDataInfos.end()) return it->second;
+    return std::nullopt;
+}
+
+std::optional<MediaFileInfo> getMediaFileInfoForConfig(const AudioPortConfig& config) {
+    if (!config.sampleRate.has_value() || !config.format.has_value() ||
+        !config.channelMask.has_value()) {
+        return std::nullopt;
+    }
+    return getMediaFileInfoForConfig(AudioConfigBase{
+            config.sampleRate->value, config.channelMask.value(), config.format.value()});
+}
+
+std::optional<AudioOffloadInfo> generateOffloadInfoIfNeeded(const AudioPortConfig& portConfig) {
+    if (portConfig.flags.has_value() &&
+        portConfig.flags.value().getTag() == AudioIoFlags::Tag::output &&
+        isBitPositionFlagSet(portConfig.flags.value().get<AudioIoFlags::Tag::output>(),
+                             AudioOutputFlags::COMPRESS_OFFLOAD)) {
+        AudioOffloadInfo offloadInfo;
+        offloadInfo.base.sampleRate = portConfig.sampleRate.value().value;
+        offloadInfo.base.channelMask = portConfig.channelMask.value();
+        offloadInfo.base.format = portConfig.format.value();
+        if (auto info = getMediaFileInfoForConfig(portConfig); info.has_value()) {
+            offloadInfo.bitRatePerSecond = info->bps;
+            offloadInfo.durationUs = info->durationMs * 1000LL;
+        } else {
+            offloadInfo.bitRatePerSecond = 256000;                             // Arbitrary value.
+            offloadInfo.durationUs = std::chrono::microseconds(1min).count();  // Arbitrary value.
+        }
+        offloadInfo.usage = AudioUsage::MEDIA;
+        offloadInfo.encapsulationMode = AudioEncapsulationMode::NONE;
+        return offloadInfo;
+    }
+    return {};
+}
+
 // All 'With*' classes are move-only because they are associated with some
 // resource or state of a HAL module.
 class WithDebugFlags {
@@ -652,11 +710,14 @@
     typedef AidlMessageQueue<StreamDescriptor::Reply, SynchronizedReadWrite> ReplyMQ;
     typedef AidlMessageQueue<int8_t, SynchronizedReadWrite> DataMQ;
 
-    explicit StreamContext(const StreamDescriptor& descriptor)
+    explicit StreamContext(const StreamDescriptor& descriptor, const AudioConfigBase& config,
+                           AudioIoFlags flags)
         : mFrameSizeBytes(descriptor.frameSizeBytes),
+          mConfig(config),
           mCommandMQ(new CommandMQ(descriptor.command)),
           mReplyMQ(new ReplyMQ(descriptor.reply)),
           mBufferSizeFrames(descriptor.bufferSizeFrames),
+          mFlags(flags),
           mDataMQ(maybeCreateDataMQ(descriptor)),
           mIsMmapped(isMmapped(descriptor)),
           mSharedMemoryFd(maybeGetMmapFd(descriptor)) {
@@ -695,9 +756,12 @@
     size_t getBufferSizeBytes() const { return mFrameSizeBytes * mBufferSizeFrames; }
     size_t getBufferSizeFrames() const { return mBufferSizeFrames; }
     CommandMQ* getCommandMQ() const { return mCommandMQ.get(); }
+    const AudioConfigBase& getConfig() const { return mConfig; }
     DataMQ* getDataMQ() const { return mDataMQ.get(); }
+    AudioIoFlags getFlags() const { return mFlags; }
     size_t getFrameSizeBytes() const { return mFrameSizeBytes; }
     ReplyMQ* getReplyMQ() const { return mReplyMQ.get(); }
+    int getSampleRate() const { return mConfig.sampleRate; }
     bool isMmapped() const { return mIsMmapped; }
     int8_t* getMmapMemory() const { return mSharedMemory; }
 
@@ -722,9 +786,11 @@
     }
 
     const size_t mFrameSizeBytes;
+    const AudioConfigBase mConfig;
     std::unique_ptr<CommandMQ> mCommandMQ;
     std::unique_ptr<ReplyMQ> mReplyMQ;
     const size_t mBufferSizeFrames;
+    const AudioIoFlags mFlags;
     std::unique_ptr<DataMQ> mDataMQ;
     const bool mIsMmapped;
     const int32_t mSharedMemoryFd;
@@ -926,12 +992,19 @@
           mDriver(driver),
           mEventReceiver(eventReceiver),
           mIsMmapped(context.isMmapped()),
-          mSharedMemory(context.getMmapMemory()) {}
+          mSharedMemory(context.getMmapMemory()),
+          mIsCompressOffload(context.getFlags().getTag() == AudioIoFlags::output &&
+                             isBitPositionFlagSet(context.getFlags().get<AudioIoFlags::output>(),
+                                                  AudioOutputFlags::COMPRESS_OFFLOAD)),
+          mConfig(context.getConfig()) {}
     StreamContext::CommandMQ* getCommandMQ() const { return mCommandMQ; }
+    const AudioConfigBase& getConfig() const { return mConfig; }
     StreamContext::ReplyMQ* getReplyMQ() const { return mReplyMQ; }
     StreamContext::DataMQ* getDataMQ() const { return mDataMQ; }
     StreamLogicDriver* getDriver() const { return mDriver; }
     StreamEventReceiver* getEventReceiver() const { return mEventReceiver; }
+    int getSampleRate() const { return mConfig.sampleRate; }
+    bool isCompressOffload() const { return mIsCompressOffload; }
     bool isMmapped() const { return mIsMmapped; }
 
     std::string init() override {
@@ -940,6 +1013,10 @@
     }
     const std::vector<int8_t>& getData() const { return mData; }
     void fillData(int8_t filler) { std::fill(mData.begin(), mData.end(), filler); }
+    void loadData(std::ifstream& is, size_t* size) {
+        *size = std::min(*size, mData.size());
+        is.read(reinterpret_cast<char*>(mData.data()), *size);
+    }
     std::optional<StreamDescriptor::Command> maybeGetNextCommand(int* actualSize = nullptr) {
         TransitionTrigger trigger = mDriver->getNextTrigger(mData.size(), actualSize);
         if (StreamEventReceiver::Event* expEvent =
@@ -1002,6 +1079,8 @@
     int mLastEventSeq = StreamEventReceiver::kEventSeqInit;
     const bool mIsMmapped;
     int8_t* mSharedMemory = nullptr;
+    const bool mIsCompressOffload;
+    const AudioConfigBase mConfig;
 };
 
 class StreamReaderLogic : public StreamCommonLogic {
@@ -1102,6 +1181,24 @@
     const std::vector<int8_t>& getData() const { return StreamCommonLogic::getData(); }
 
   protected:
+    std::string init() override {
+        if (auto status = StreamCommonLogic::init(); !status.empty()) return status;
+        if (isCompressOffload()) {
+            const auto info = getMediaFileInfoForConfig(getConfig());
+            if (info) {
+                mCompressedMedia.open(info->path, std::ios::in | std::ios::binary);
+                if (!mCompressedMedia.is_open()) {
+                    return std::string("failed to open media file \"") + info->path + "\"";
+                }
+                mCompressedMedia.seekg(0, mCompressedMedia.end);
+                mCompressedMediaSize = mCompressedMedia.tellg();
+                mCompressedMedia.seekg(0, mCompressedMedia.beg);
+                LOG(DEBUG) << __func__ << ": using media file \"" << info->path << "\", size "
+                           << mCompressedMediaSize << " bytes";
+            }
+        }
+        return "";
+    }
     Status cycle() override {
         if (getDriver()->done()) {
             LOG(DEBUG) << __func__ << ": clean exit";
@@ -1115,13 +1212,31 @@
             LOG(ERROR) << __func__ << ": no next command";
             return Status::ABORT;
         }
-        if (actualSize != 0) {
+        if (actualSize > 0) {
             if (command.getTag() == StreamDescriptor::Command::burst) {
-                fillData(mBurstIteration);
-                if (mBurstIteration < std::numeric_limits<int8_t>::max()) {
-                    mBurstIteration++;
+                if (!isCompressOffload()) {
+                    fillData(mBurstIteration);
+                    if (mBurstIteration < std::numeric_limits<int8_t>::max()) {
+                        mBurstIteration++;
+                    } else {
+                        mBurstIteration = 0;
+                    }
                 } else {
-                    mBurstIteration = 0;
+                    fillData(0);
+                    size_t size = std::min(static_cast<size_t>(actualSize),
+                                           mCompressedMediaSize - mCompressedMediaPos);
+                    loadData(mCompressedMedia, &size);
+                    if (!mCompressedMedia.good()) {
+                        LOG(ERROR) << __func__ << ": read failed";
+                        return Status::ABORT;
+                    }
+                    LOG(DEBUG) << __func__ << ": read from file " << size << " bytes";
+                    mCompressedMediaPos += size;
+                    if (mCompressedMediaPos >= mCompressedMediaSize) {
+                        mCompressedMedia.seekg(0, mCompressedMedia.beg);
+                        mCompressedMediaPos = 0;
+                        LOG(DEBUG) << __func__ << ": rewound to the beginning of the file";
+                    }
                 }
             }
             if (isMmapped() ? !writeDataToMmap() : !writeDataToMQ()) {
@@ -1185,6 +1300,9 @@
 
   private:
     int8_t mBurstIteration = 1;
+    std::ifstream mCompressedMedia;
+    size_t mCompressedMediaSize = 0;
+    size_t mCompressedMediaPos = 0;
 };
 using StreamWriter = StreamWorker<StreamWriterLogic>;
 
@@ -1293,7 +1411,13 @@
         ASSERT_NE(nullptr, mStream) << "port config id " << getPortId();
         EXPECT_GE(mDescriptor.bufferSizeFrames, bufferSizeFrames)
                 << "actual buffer size must be no less than requested";
-        mContext.emplace(mDescriptor);
+        const auto& config = mPortConfig.get();
+        ASSERT_TRUE(config.channelMask.has_value());
+        ASSERT_TRUE(config.format.has_value());
+        ASSERT_TRUE(config.sampleRate.has_value());
+        ASSERT_TRUE(config.flags.has_value());
+        const AudioConfigBase cfg{config.sampleRate->value, *config.channelMask, *config.format};
+        mContext.emplace(mDescriptor, cfg, config.flags.value());
         ASSERT_NO_FATAL_FAILURE(mContext.value().checkIsValid());
     }
     void SetUp(IModule* module, long bufferSizeFrames) {
@@ -1364,7 +1488,7 @@
     aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args;
     args.portConfigId = portConfig.id;
     args.sourceMetadata = GenerateSourceMetadata(portConfig);
-    args.offloadInfo = ModuleConfig::generateOffloadInfoIfNeeded(portConfig);
+    args.offloadInfo = generateOffloadInfoIfNeeded(portConfig);
     args.bufferSizeFrames = bufferSizeFrames;
     auto callback = ndk::SharedRefBase::make<DefaultStreamCallback>();
     args.callback = callback;
@@ -3192,10 +3316,12 @@
                                     {AudioInputFlags::MMAP_NOIRQ, AudioInputFlags::VOIP_TX,
                                      AudioInputFlags::HW_HOTWORD, AudioInputFlags::HOTWORD_TAP})) ||
            (portConfig.flags.value().getTag() == AudioIoFlags::output &&
-            isAnyBitPositionFlagSet(
-                    portConfig.flags.value().template get<AudioIoFlags::output>(),
-                    {AudioOutputFlags::MMAP_NOIRQ, AudioOutputFlags::VOIP_RX,
-                     AudioOutputFlags::COMPRESS_OFFLOAD, AudioOutputFlags::INCALL_MUSIC}));
+            (isAnyBitPositionFlagSet(portConfig.flags.value().template get<AudioIoFlags::output>(),
+                                     {AudioOutputFlags::MMAP_NOIRQ, AudioOutputFlags::VOIP_RX,
+                                      AudioOutputFlags::INCALL_MUSIC}) ||
+             (isBitPositionFlagSet(portConfig.flags.value().template get<AudioIoFlags::output>(),
+                                   AudioOutputFlags::COMPRESS_OFFLOAD) &&
+              !getMediaFileInfoForConfig(portConfig))));
 }
 
 // Certain types of devices can not be used without special preconditions.
@@ -3863,7 +3989,7 @@
     aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args;
     args.portConfigId = portConfig.id;
     args.sourceMetadata = GenerateSourceMetadata(portConfig);
-    args.offloadInfo = ModuleConfig::generateOffloadInfoIfNeeded(portConfig);
+    args.offloadInfo = generateOffloadInfoIfNeeded(portConfig);
     args.bufferSizeFrames = stream.getPatch().minimumStreamBufferSizeFrames;
     aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret;
     EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, module->openOutputStream(args, &ret))
@@ -4185,18 +4311,6 @@
                     std::get<NAMED_CMD_DELAY_MS>(std::get<PARAM_CMD_SEQ>(GetParam()));
             ASSERT_NO_FATAL_FAILURE(delayTransientStates.SetUp(module.get()));
             ASSERT_NO_FATAL_FAILURE(runStreamIoCommands(portConfig));
-            if (aidlVersion >= kAidlVersion3 && isNonBlocking && !IOTraits<Stream>::is_input) {
-                // Also try running the same sequence with "aosp.forceDrainToDraining" set.
-                // This will only work with the default implementation. When it works, the stream
-                // tries always to move to the 'DRAINING' state after an "early notify" drain.
-                // This helps to check more paths for our test scenarios.
-                WithModuleParameter forceDrainToDraining("aosp.forceDrainToDraining",
-                                                         Boolean{true});
-                if (forceDrainToDraining.SetUpNoChecks(module.get(), true /*failureExpected*/)
-                            .isOk()) {
-                    ASSERT_NO_FATAL_FAILURE(runStreamIoCommands(portConfig));
-                }
-            }
             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
@@ -4744,9 +4858,14 @@
 std::shared_ptr<StateSequence> makeDrainEarlyOutCommands() {
     using State = StreamDescriptor::State;
     auto d = std::make_unique<StateDag>();
-    StateDag::Node last = d->makeFinalNode(State::IDLE);
-    StateDag::Node draining = d->makeNode(State::DRAINING, kDrainReadyEvent, last);
-    draining.children().push_back(d->makeNode(State::DRAINING, kGetStatusCommand, last));
+    // In the "early notify" case, the transition to the `IDLE` state following
+    // the 'onDrainReady' event can take some time. Waiting for an arbitrary amount
+    // of time may make the test fragile. Instead, for successful completion
+    // is registered if the stream has entered `IDLE` or `DRAINING` state.
+    StateDag::Node lastIdle = d->makeFinalNode(State::IDLE);
+    StateDag::Node lastDraining = d->makeFinalNode(State::DRAINING);
+    StateDag::Node draining =
+            d->makeNode(State::DRAINING, kDrainReadyEvent, lastIdle, lastDraining);
     StateDag::Node active = d->makeNode(State::ACTIVE, kDrainOutEarlyCommand, draining);
     StateDag::Node idle = d->makeNode(State::IDLE, kBurstCommand, active);
     idle.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, active));
diff --git a/audio/aidl/vts/VtsHalAudioCoreTargetTestTemplate.xml b/audio/aidl/vts/VtsHalAudioCoreTargetTestTemplate.xml
new file mode 100644
index 0000000..94db58d
--- /dev/null
+++ b/audio/aidl/vts/VtsHalAudioCoreTargetTestTemplate.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 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.
+-->
+<configuration description="Runs {MODULE}.">
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-native" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.StopServicesSetup"/>
+
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="setprop vts.native_server.on 1"/>
+        <option name="teardown-command" value="setprop vts.native_server.on 0"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push" value="{MODULE}->/data/local/tmp/{MODULE}" />
+        <option name="push" value="sine882hz_44100_3s.ape->/data/local/tmp/sine882hz_44100_3s.ape" />
+        <option name="push" value="sine960hz_48000_3s.ape->/data/local/tmp/sine960hz_48000_3s.ape" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="{MODULE}" />
+        <option name="native-test-timeout" value="30m" />
+    </test>
+</configuration>
diff --git a/audio/aidl/vts/VtsHalAudioTargetTestTemplate.xml b/audio/aidl/vts/VtsHalAudioEffectTargetTestTemplate.xml
similarity index 100%
rename from audio/aidl/vts/VtsHalAudioTargetTestTemplate.xml
rename to audio/aidl/vts/VtsHalAudioEffectTargetTestTemplate.xml
diff --git a/audio/aidl/vts/VtsHalHapticGeneratorTargetTest.cpp b/audio/aidl/vts/VtsHalHapticGeneratorTargetTest.cpp
index 2802bf9..1b0b681 100644
--- a/audio/aidl/vts/VtsHalHapticGeneratorTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalHapticGeneratorTargetTest.cpp
@@ -275,6 +275,9 @@
 enum DataTestParam { EFFECT_INSTANCE, LAYOUT };
 using HapticGeneratorDataTestParam = std::tuple<EffectInstance, int32_t>;
 
+// minimal HAL interface version to run the data path test
+constexpr int32_t kMinDataTestHalVersion = 3;
+
 class HapticGeneratorDataTest : public ::testing::TestWithParam<HapticGeneratorDataTestParam>,
                                 public HapticGeneratorHelper {
   public:
@@ -293,7 +296,14 @@
         mOutput.resize(mHapticSamples + mAudioSamples, 0);
     }
 
-    void SetUp() override { ASSERT_NO_FATAL_FAILURE(SetUpHapticGenerator(mChMask)); }
+    void SetUp() override {
+        ASSERT_NO_FATAL_FAILURE(SetUpHapticGenerator(mChMask));
+        if (int32_t version;
+            mEffect->getInterfaceVersion(&version).isOk() && version < kMinDataTestHalVersion) {
+            GTEST_SKIP() << "Skipping the data test for version: " << version << "\n";
+        }
+    }
+
     void TearDown() override { ASSERT_NO_FATAL_FAILURE(TearDownHapticGenerator()); }
 
     void generateSinePeriod() {
diff --git a/audio/aidl/vts/data/sine882hz_44100_3s.ape b/audio/aidl/vts/data/sine882hz_44100_3s.ape
new file mode 100644
index 0000000..1cefb15
--- /dev/null
+++ b/audio/aidl/vts/data/sine882hz_44100_3s.ape
Binary files differ
diff --git a/audio/aidl/vts/data/sine960hz_48000_3s.ape b/audio/aidl/vts/data/sine960hz_48000_3s.ape
new file mode 100644
index 0000000..149c42a
--- /dev/null
+++ b/audio/aidl/vts/data/sine960hz_48000_3s.ape
Binary files differ
diff --git a/automotive/can/OWNERS b/automotive/can/OWNERS
index ffa4828..b738dac 100644
--- a/automotive/can/OWNERS
+++ b/automotive/can/OWNERS
@@ -1,3 +1,2 @@
-kevinme@google.com
 chrisweir@google.com
 twasilczyk@google.com
diff --git a/automotive/vehicle/OWNERS b/automotive/vehicle/OWNERS
index f099287..066af9a 100644
--- a/automotive/vehicle/OWNERS
+++ b/automotive/vehicle/OWNERS
@@ -1,9 +1,6 @@
 ericjeong@google.com
 shanyu@google.com
 
-# GRPC VHAL
-per-file aidl/impl/grpc/** =egranata@google.com
-
 # Property definition
 per-file aidl_property/** = tylertrephan@google.com
 per-file aidl/generated_lib/** = tylertrephan@google.com
diff --git a/power/stats/1.0/default/OWNERS b/power/stats/1.0/default/OWNERS
index 2d95a97..0557220 100644
--- a/power/stats/1.0/default/OWNERS
+++ b/power/stats/1.0/default/OWNERS
@@ -1,3 +1,2 @@
 krossmo@google.com
 bsschwar@google.com
-tstrudel@google.com