Merge changes from topic "feature_dynamicVolSupport"

* changes:
  Adding VTS tests for IAudioControl#registerModuleChangeCallback
  Add default implementation for dynamic volume config feature
  Adding support for dynamic audio gain config
diff --git a/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/current/android/hardware/automotive/audiocontrol/IAudioControl.aidl b/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/current/android/hardware/automotive/audiocontrol/IAudioControl.aidl
index 8dc5ffe..ffd575d 100644
--- a/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/current/android/hardware/automotive/audiocontrol/IAudioControl.aidl
+++ b/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/current/android/hardware/automotive/audiocontrol/IAudioControl.aidl
@@ -46,4 +46,6 @@
   oneway void onAudioFocusChangeWithMetaData(in android.hardware.audio.common.PlaybackTrackMetadata playbackMetaData, in int zoneId, in android.hardware.automotive.audiocontrol.AudioFocusChange focusChange);
   oneway void setAudioDeviceGainsChanged(in android.hardware.automotive.audiocontrol.Reasons[] reasons, in android.hardware.automotive.audiocontrol.AudioGainConfigInfo[] gains);
   oneway void registerGainCallback(in android.hardware.automotive.audiocontrol.IAudioGainCallback callback);
+  void setModuleChangeCallback(in android.hardware.automotive.audiocontrol.IModuleChangeCallback callback);
+  void clearModuleChangeCallback();
 }
diff --git a/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/current/android/hardware/automotive/audiocontrol/IModuleChangeCallback.aidl b/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/current/android/hardware/automotive/audiocontrol/IModuleChangeCallback.aidl
new file mode 100644
index 0000000..2bbb936
--- /dev/null
+++ b/automotive/audiocontrol/aidl/aidl_api/android.hardware.automotive.audiocontrol/current/android/hardware/automotive/audiocontrol/IModuleChangeCallback.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.hardware.automotive.audiocontrol;
+@VintfStability
+interface IModuleChangeCallback {
+  oneway void onAudioPortsChanged(in android.media.audio.common.AudioPort[] audioPorts);
+}
diff --git a/automotive/audiocontrol/aidl/android/hardware/automotive/audiocontrol/IAudioControl.aidl b/automotive/audiocontrol/aidl/android/hardware/automotive/audiocontrol/IAudioControl.aidl
index 0ffcd5e..9564efc 100644
--- a/automotive/audiocontrol/aidl/android/hardware/automotive/audiocontrol/IAudioControl.aidl
+++ b/automotive/audiocontrol/aidl/android/hardware/automotive/audiocontrol/IAudioControl.aidl
@@ -51,6 +51,7 @@
 import android.hardware.automotive.audiocontrol.DuckingInfo;
 import android.hardware.automotive.audiocontrol.IAudioGainCallback;
 import android.hardware.automotive.audiocontrol.IFocusListener;
+import android.hardware.automotive.audiocontrol.IModuleChangeCallback;
 import android.hardware.automotive.audiocontrol.MutingInfo;
 import android.hardware.automotive.audiocontrol.Reasons;
 
@@ -183,4 +184,26 @@
      *                 interface.
      */
     oneway void registerGainCallback(in IAudioGainCallback callback);
+
+    /**
+     * Sets callback with HAL for notifying changes to hardware module (that is:
+     * {@link android.hardware.audio.core.IModule}) configurations.
+     *
+     * @param callback The {@link android.hardware.automotive.audiocontrol.IModuleChangeCallback}
+     *                 interface to use use when new updates are available for
+     *                 {@link android.hardware.audio.core.IModule} configs
+     * @throws EX_UNSUPPORTED_OPERATION if dynamic audio configs are not supported.
+     * @throws EX_ILLEGAL_STATE if a callback already exists
+     * @throws EX_ILLEGAL_ARGUMENT if the passed callback is (@code null}
+     */
+    void setModuleChangeCallback(in IModuleChangeCallback callback);
+
+    /**
+     * Clears module change callback
+     *
+     * If no callback is registered previously, then this call should be a no-op.
+     *
+     * @throws EX_UNSUPPORTED_OPERATION if dynamic audio configs are not supported.
+     */
+    void clearModuleChangeCallback();
 }
diff --git a/automotive/audiocontrol/aidl/android/hardware/automotive/audiocontrol/IModuleChangeCallback.aidl b/automotive/audiocontrol/aidl/android/hardware/automotive/audiocontrol/IModuleChangeCallback.aidl
new file mode 100644
index 0000000..4282483
--- /dev/null
+++ b/automotive/audiocontrol/aidl/android/hardware/automotive/audiocontrol/IModuleChangeCallback.aidl
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.automotive.audiocontrol;
+
+import android.media.audio.common.AudioPort;
+
+/**
+ * Interface definition for asynchronous changes to audio configs defined
+ * for a hardware {@link android.hardware.audio.core.IModule}.
+ */
+@VintfStability
+oneway interface IModuleChangeCallback {
+    /**
+     * Used to indicate that one or more {@link android.media.audio.common.AudioPort}
+     * configs have changed. Implementations MUST return at least one AudioPort.
+     *
+     * Notes for AudioPort:
+     * 1. For V3, the support will be limited to configurable AudioGain stages - per
+     *    car audio framework support.
+     * 2. For automotive 'bus' devices, the expected settings are
+     *     AudioDevice {
+     *        AudioDeviceDescription {type: IN/OUT_DEVICE, connection: CONNECTION_BUS}
+     *        AudioDeviceAddress {id: string}}
+     *
+     * Notes for AudioGain:
+     * 1. Car audio framework only supports AudioGainMode::JOINT. Any other mode
+     *    selection will be ignored.
+     *    See {@link android.media.audio.common.AudioGainMode}
+     * 2. Implementations MUST ensure that the gain stages are identical for buses
+     *    that map to the same volume group. Any inconsistencies will result in
+     *    inferior volume-change experience to the users.
+     * 3. Implementations MUST ensure that the new gain stages are subset (do not
+     *    exceed) of the gain stage definitions provided to audio policy. If they
+     *    exceed, it can result in failure when setting value over the range
+     *    allowed by the audio policy.
+     *
+     * Other notes:
+     * 1. In case of AudioControl  service restart or resume, the implementations MUST
+     *    issue an immediate callback following registration.
+     * 2. In case of client restart, the AudioControl service MUST clear all stale
+     *    callbacks.
+     *
+     * @param audioPorts list of {@link android.media.audio.common.AudioPort} that
+     *                   are updated
+     */
+    void onAudioPortsChanged(in AudioPort[] audioPorts);
+}
diff --git a/automotive/audiocontrol/aidl/default/Android.bp b/automotive/audiocontrol/aidl/default/Android.bp
index 6bf4b9d..dde05c3 100644
--- a/automotive/audiocontrol/aidl/default/Android.bp
+++ b/automotive/audiocontrol/aidl/default/Android.bp
@@ -31,7 +31,7 @@
         "android.hardware.audio.common@7.0-enums",
         "android.hardware.audio.common-V1-ndk",
         "android.frameworks.automotive.powerpolicy-V1-ndk",
-        "android.hardware.automotive.audiocontrol-V2-ndk",
+        "android.hardware.automotive.audiocontrol-V3-ndk",
         "libbase",
         "libbinder_ndk",
         "libcutils",
diff --git a/automotive/audiocontrol/aidl/default/AudioControl.cpp b/automotive/audiocontrol/aidl/default/AudioControl.cpp
index a121f8b..cf7307d 100644
--- a/automotive/audiocontrol/aidl/default/AudioControl.cpp
+++ b/automotive/audiocontrol/aidl/default/AudioControl.cpp
@@ -24,6 +24,7 @@
 #include <aidl/android/hardware/automotive/audiocontrol/IFocusListener.h>
 
 #include <android-base/logging.h>
+#include <android-base/parsebool.h>
 #include <android-base/parseint.h>
 #include <android-base/strings.h>
 
@@ -37,6 +38,8 @@
 namespace aidl::android::hardware::automotive::audiocontrol {
 
 using ::android::base::EqualsIgnoreCase;
+using ::android::base::ParseBool;
+using ::android::base::ParseBoolResult;
 using ::android::base::ParseInt;
 using ::std::shared_ptr;
 using ::std::string;
@@ -70,6 +73,95 @@
 }
 }  // namespace
 
+namespace {
+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::AudioFormatDescription;
+using ::aidl::android::media::audio::common::AudioFormatType;
+using ::aidl::android::media::audio::common::AudioGain;
+using ::aidl::android::media::audio::common::AudioGainMode;
+using ::aidl::android::media::audio::common::AudioIoFlags;
+using ::aidl::android::media::audio::common::AudioOutputFlags;
+using ::aidl::android::media::audio::common::AudioPort;
+using ::aidl::android::media::audio::common::AudioPortDeviceExt;
+using ::aidl::android::media::audio::common::AudioPortExt;
+using ::aidl::android::media::audio::common::AudioPortMixExt;
+using ::aidl::android::media::audio::common::AudioProfile;
+using ::aidl::android::media::audio::common::PcmType;
+
+// reuse common code artifacts
+void fillProfile(const std::vector<int32_t>& channelLayouts,
+                 const std::vector<int32_t>& sampleRates, AudioProfile* profile) {
+    for (auto layout : channelLayouts) {
+        profile->channelMasks.push_back(
+                AudioChannelLayout::make<AudioChannelLayout::layoutMask>(layout));
+    }
+    profile->sampleRates.insert(profile->sampleRates.end(), sampleRates.begin(), sampleRates.end());
+}
+
+AudioProfile createProfile(PcmType pcmType, const std::vector<int32_t>& channelLayouts,
+                           const std::vector<int32_t>& sampleRates) {
+    AudioProfile profile;
+    profile.format.type = AudioFormatType::PCM;
+    profile.format.pcm = pcmType;
+    fillProfile(channelLayouts, sampleRates, &profile);
+    return profile;
+}
+
+AudioProfile createProfile(const std::string& encodingType,
+                           const std::vector<int32_t>& channelLayouts,
+                           const std::vector<int32_t>& sampleRates) {
+    AudioProfile profile;
+    profile.format.encoding = encodingType;
+    fillProfile(channelLayouts, sampleRates, &profile);
+    return profile;
+}
+
+AudioPortExt createDeviceExt(AudioDeviceType devType, int32_t flags,
+                             const std::string& connection = "", const std::string& address = "") {
+    AudioPortDeviceExt deviceExt;
+    deviceExt.device.type.type = devType;
+    if (devType == AudioDeviceType::IN_MICROPHONE && connection.empty()) {
+        deviceExt.device.address = "bottom";
+    } else if (devType == AudioDeviceType::IN_MICROPHONE_BACK && connection.empty()) {
+        deviceExt.device.address = "back";
+    } else {
+        deviceExt.device.address = std::move(address);
+    }
+    deviceExt.device.type.connection = std::move(connection);
+    deviceExt.flags = flags;
+    return AudioPortExt::make<AudioPortExt::Tag::device>(deviceExt);
+}
+
+AudioPort createPort(int32_t id, const std::string& name, int32_t flags, bool isInput,
+                     const AudioPortExt& ext) {
+    AudioPort port;
+    port.id = id;
+    port.name = name;
+    port.flags = isInput ? AudioIoFlags::make<AudioIoFlags::Tag::input>(flags)
+                         : AudioIoFlags::make<AudioIoFlags::Tag::output>(flags);
+    port.ext = ext;
+    return port;
+}
+
+AudioGain createGain(int32_t mode, AudioChannelLayout channelMask, int32_t minValue,
+                     int32_t maxValue, int32_t defaultValue, int32_t stepValue,
+                     int32_t minRampMs = 100, int32_t maxRampMs = 100, bool useForVolume = true) {
+    AudioGain gain;
+    gain.mode = mode;
+    gain.channelMask = channelMask;
+    gain.minValue = minValue;
+    gain.maxValue = maxValue;
+    gain.defaultValue = defaultValue;
+    gain.stepValue = stepValue;
+    gain.minRampMs = minRampMs;
+    gain.maxRampMs = maxRampMs;
+    gain.useForVolume = useForVolume;
+    return gain;
+}
+}  // namespace
+
 ndk::ScopedAStatus AudioControl::registerFocusListener(
         const shared_ptr<IFocusListener>& in_listener) {
     LOG(DEBUG) << "registering focus listener";
@@ -190,6 +282,33 @@
     return ndk::ScopedAStatus::ok();
 }
 
+ndk::ScopedAStatus AudioControl::setModuleChangeCallback(
+        const std::shared_ptr<IModuleChangeCallback>& in_callback) {
+    LOG(DEBUG) << ": " << __func__;
+
+    if (in_callback.get() == nullptr) {
+        LOG(ERROR) << __func__ << ": Callback is nullptr";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT);
+    }
+    if (mModuleChangeCallback != nullptr) {
+        LOG(ERROR) << __func__ << ": Module change callback was already registered";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    std::atomic_store(&mModuleChangeCallback, in_callback);
+    return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus AudioControl::clearModuleChangeCallback() {
+    if (mModuleChangeCallback != nullptr) {
+        shared_ptr<IModuleChangeCallback> nullCallback = nullptr;
+        std::atomic_store(&mModuleChangeCallback, nullCallback);
+        LOG(DEBUG) << ":" << __func__ << ": Unregistered successfully";
+    } else {
+        LOG(DEBUG) << ":" << __func__ << ": No callback registered, no-op";
+    }
+    return ndk::ScopedAStatus::ok();
+}
+
 binder_status_t AudioControl::dump(int fd, const char** args, uint32_t numArgs) {
     if (numArgs == 0) {
         return dumpsys(fd);
@@ -208,6 +327,8 @@
         return cmdAbandonFocusWithMetaData(fd, args, numArgs);
     } else if (EqualsIgnoreCase(option, "--audioGainCallback")) {
         return cmdOnAudioDeviceGainsChanged(fd, args, numArgs);
+    } else if (EqualsIgnoreCase(option, "--audioPortsChangedCallback")) {
+        return cmdOnAudioPortsChanged(fd, args, numArgs);
     } else {
         dprintf(fd, "Invalid option: %s\n", option.c_str());
         return STATUS_BAD_VALUE;
@@ -262,7 +383,21 @@
     dprintf(fd,
             "Tags are optional. If provided, it must follow the <key>=<value> pattern, where the "
             "value is namespaced (for example com.google.strategy=VR).\n");
-
+    dprintf(fd,
+            "--audioPortsChangedCallback <ID_1> <NAME_1> <BUS_ADDRESS_1> <CONNECTION_TYPE_1> "
+            "<AUDIO_GAINS_1> [<ID_N> <NAME_N> <BUS_ADDRESS_N> <CONNECTION_TYPE_N> "
+            "<AUDIO_GAINS_N>]: fires audio ports changed callback. Carries list of modified "
+            "AudioPorts. "
+            "For simplicity, this command accepts limited information for each AudioPort: "
+            "id(int), name(string), port address(string), connection type (string), "
+            "audio gain (csv int)\n");
+    dprintf(fd, "Notes: \n");
+    dprintf(fd,
+            "1. AudioGain csv should match definition at "
+            "android/media/audio/common/AudioPort.aidl\n");
+    dprintf(fd,
+            "2. See android/media/audio/common/AudioDeviceDescription.aidl for valid "
+            "<CONNECTION_TYPE> values.\n");
     return STATUS_OK;
 }
 
@@ -514,4 +649,166 @@
             toEnumString(reasons).c_str(), toString(agcis).c_str());
     return STATUS_OK;
 }
+
+binder_status_t AudioControl::parseAudioGains(int fd, const std::string& stringGain,
+                                              std::vector<AudioGain>& gains) {
+    const int kAudioGainSize = 9;
+    std::stringstream csvGain(stringGain);
+    std::vector<std::string> vecGain;
+    std::string value;
+    while (getline(csvGain, value, ',')) {
+        vecGain.push_back(value);
+    }
+
+    if ((vecGain.size() == 0) || ((vecGain.size() % kAudioGainSize) != 0)) {
+        dprintf(fd, "Erroneous input to generate AudioGain: %s\n", stringGain.c_str());
+        return STATUS_BAD_VALUE;
+    }
+
+    // iterate over injected AudioGains
+    for (int index = 0; index < vecGain.size(); index += kAudioGainSize) {
+        int32_t mode;
+        if (!safelyParseInt(vecGain[index], &mode)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n", vecGain[index].c_str());
+            return STATUS_BAD_VALUE;
+        }
+
+        // car audio framework only supports JOINT mode.
+        // skip injected AudioGains that are not compliant with this.
+        if (mode != static_cast<int>(AudioGainMode::JOINT)) {
+            LOG(WARNING) << ":" << __func__ << ": skipping gain since it is not JOINT mode!";
+            continue;
+        }
+
+        int32_t layout;
+        if (!safelyParseInt(vecGain[index + 1], &layout)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n",
+                    vecGain[index + 1].c_str());
+            return STATUS_BAD_VALUE;
+        }
+        AudioChannelLayout channelMask =
+                AudioChannelLayout::make<AudioChannelLayout::layoutMask>(layout);
+
+        int32_t minValue;
+        if (!safelyParseInt(vecGain[index + 2], &minValue)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n",
+                    vecGain[index + 2].c_str());
+            return STATUS_BAD_VALUE;
+        }
+
+        int32_t maxValue;
+        if (!safelyParseInt(vecGain[index + 3], &maxValue)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n",
+                    vecGain[index + 3].c_str());
+            return STATUS_BAD_VALUE;
+        }
+
+        int32_t defaultValue;
+        if (!safelyParseInt(vecGain[index + 4], &defaultValue)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n",
+                    vecGain[index + 4].c_str());
+            return STATUS_BAD_VALUE;
+        }
+
+        int32_t stepValue;
+        if (!safelyParseInt(vecGain[index + 5], &stepValue)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n",
+                    vecGain[index + 5].c_str());
+            return STATUS_BAD_VALUE;
+        }
+
+        int32_t minRampMs;
+        if (!safelyParseInt(vecGain[index + 6], &minRampMs)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n",
+                    vecGain[index + 6].c_str());
+            return STATUS_BAD_VALUE;
+        }
+
+        int32_t maxRampMs;
+        if (!safelyParseInt(vecGain[index + 7], &maxRampMs)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n",
+                    vecGain[index + 7].c_str());
+            return STATUS_BAD_VALUE;
+        }
+
+        ParseBoolResult useForVolume = ParseBool(vecGain[index + 8]);
+        if (useForVolume == ParseBoolResult::kError) {
+            dprintf(fd, "Non-boolean index provided with request: %s\n",
+                    vecGain[index + 8].c_str());
+            return STATUS_BAD_VALUE;
+        } else if (useForVolume == ParseBoolResult::kFalse) {
+            // at this level we only care about gain stages that are relevant
+            // for volume control. skip the gain stage if its flagged as false.
+            LOG(WARNING) << ":" << __func__
+                         << ": skipping gain since it is not for volume control!";
+            continue;
+        }
+
+        AudioGain gain = createGain(mode, channelMask, minValue, maxValue, defaultValue, stepValue,
+                                    minRampMs, maxRampMs, true /*useForVolume*/);
+        gains.push_back(gain);
+    }
+    return STATUS_OK;
+}
+
+binder_status_t AudioControl::cmdOnAudioPortsChanged(int fd, const char** args, uint32_t numArgs) {
+    if (!checkCallerHasWritePermissions(fd)) {
+        return STATUS_PERMISSION_DENIED;
+    }
+
+    if ((numArgs < 6) || ((numArgs - 1) % 5 != 0)) {
+        dprintf(fd,
+                "Invalid number of arguments: please provide\n"
+                "--audioPortsChangedCallback <ID_1> <NAME_1> <BUS_ADDRESS_1> <CONNECTION_TYPE_1> "
+                "<AUDIO_GAINS_1> [<ID_N> <NAME_N> <BUS_ADDRESS_N> <CONNECTION_TYPE_N> "
+                "<AUDIO_GAINS_N>]: triggers audio ports changed callback. Carries list of "
+                "modified AudioPorts. "
+                "For simplicity, this command accepts limited information for each AudioPort: "
+                "id(int), name(string), port address(string), connection type (string), "
+                "audio gain (csv int)\n");
+        return STATUS_BAD_VALUE;
+    }
+
+    std::vector<AudioPort> ports;
+    for (uint32_t i = 1; i < numArgs; i += 5) {
+        binder_status_t status;
+        int32_t id;
+        if (!safelyParseInt(std::string(args[i]), &id)) {
+            dprintf(fd, "Non-integer index provided with request: %s\n",
+                    std::string(args[i]).c_str());
+            return STATUS_BAD_VALUE;
+        }
+
+        std::string name = std::string(args[i + 1]);
+        std::string address = std::string(args[i + 2]);
+        std::string connection = std::string(args[i + 3]);
+
+        std::string stringGains = std::string(args[i + 4]);
+        std::vector<AudioGain> gains;
+        status = parseAudioGains(fd, stringGains, gains);
+        if (status != STATUS_OK) {
+            return status;
+        }
+
+        AudioPort port = createPort(
+                id, name, 0 /*flags*/, false /*isInput*/,
+                createDeviceExt(AudioDeviceType::OUT_DEVICE, 0 /*flags*/, connection, address));
+        port.gains.insert(port.gains.begin(), gains.begin(), gains.end());
+
+        ports.push_back(port);
+    }
+
+    if (mModuleChangeCallback == nullptr) {
+        dprintf(fd,
+                "Unable to trigger audio port callback for ports: %s \n"
+                " - no module change callback registered\n",
+                toString(ports).c_str());
+        return STATUS_BAD_VALUE;
+    }
+
+    // TODO (b/269139706) fix atomic read issue
+    mModuleChangeCallback->onAudioPortsChanged(ports);
+    dprintf(fd, "SUCCESS audio port callback for ports: %s \n", toString(ports).c_str());
+    return STATUS_OK;
+}
 }  // namespace aidl::android::hardware::automotive::audiocontrol
diff --git a/automotive/audiocontrol/aidl/default/AudioControl.h b/automotive/audiocontrol/aidl/default/AudioControl.h
index 16b80e8..7eca446 100644
--- a/automotive/audiocontrol/aidl/default/AudioControl.h
+++ b/automotive/audiocontrol/aidl/default/AudioControl.h
@@ -21,11 +21,20 @@
 #include <aidl/android/hardware/automotive/audiocontrol/BnAudioControl.h>
 #include <aidl/android/hardware/automotive/audiocontrol/DuckingInfo.h>
 #include <aidl/android/hardware/automotive/audiocontrol/IAudioGainCallback.h>
+#include <aidl/android/hardware/automotive/audiocontrol/IModuleChangeCallback.h>
 #include <aidl/android/hardware/automotive/audiocontrol/MutingInfo.h>
 #include <aidl/android/hardware/automotive/audiocontrol/Reasons.h>
 
 #include <aidl/android/hardware/audio/common/PlaybackTrackMetadata.h>
 
+#include <aidl/android/media/audio/common/AudioChannelLayout.h>
+#include <aidl/android/media/audio/common/AudioDeviceType.h>
+#include <aidl/android/media/audio/common/AudioFormatDescription.h>
+#include <aidl/android/media/audio/common/AudioFormatType.h>
+#include <aidl/android/media/audio/common/AudioGainMode.h>
+#include <aidl/android/media/audio/common/AudioIoFlags.h>
+#include <aidl/android/media/audio/common/AudioOutputFlags.h>
+
 namespace aidl::android::hardware::automotive::audiocontrol {
 
 namespace audiohalcommon = ::aidl::android::hardware::audio::common;
@@ -51,6 +60,9 @@
             const std::vector<AudioGainConfigInfo>& in_gains) override;
     ndk::ScopedAStatus registerGainCallback(
             const std::shared_ptr<IAudioGainCallback>& in_callback) override;
+    ndk::ScopedAStatus setModuleChangeCallback(
+            const std::shared_ptr<IModuleChangeCallback>& in_callback) override;
+    ndk::ScopedAStatus clearModuleChangeCallback() override;
 
     binder_status_t dump(int fd, const char** args, uint32_t numArgs) override;
 
@@ -67,15 +79,23 @@
      */
     std::shared_ptr<IAudioGainCallback> mAudioGainCallback = nullptr;
 
+    std::shared_ptr<IModuleChangeCallback> mModuleChangeCallback = nullptr;
+
     binder_status_t cmdHelp(int fd) const;
     binder_status_t cmdRequestFocus(int fd, const char** args, uint32_t numArgs);
     binder_status_t cmdAbandonFocus(int fd, const char** args, uint32_t numArgs);
     binder_status_t cmdRequestFocusWithMetaData(int fd, const char** args, uint32_t numArgs);
     binder_status_t cmdAbandonFocusWithMetaData(int fd, const char** args, uint32_t numArgs);
     binder_status_t cmdOnAudioDeviceGainsChanged(int fd, const char** args, uint32_t numArgs);
+    binder_status_t cmdOnAudioPortsChanged(int fd, const char** args, uint32_t numArgs);
 
     binder_status_t parseMetaData(int fd, const std::string& metadataLiteral,
                                   audiohalcommon::PlaybackTrackMetadata& trackMetadata);
+    binder_status_t parseAudioGains(
+            int fd, const std::string& stringGain,
+            std::vector<::aidl::android::media::audio::common::AudioGain>& gains);
+    binder_status_t parseSampleRates(int fd, const std::string& sampleRates,
+                                     std::vector<int32_t>& vecSampleRates);
 
     binder_status_t dumpsys(int fd);
 };
diff --git a/automotive/audiocontrol/aidl/vts/Android.bp b/automotive/audiocontrol/aidl/vts/Android.bp
index edac160..a9122ce 100644
--- a/automotive/audiocontrol/aidl/vts/Android.bp
+++ b/automotive/audiocontrol/aidl/vts/Android.bp
@@ -39,7 +39,7 @@
         "libxml2",
     ],
     static_libs: [
-        "android.hardware.automotive.audiocontrol-V2-cpp",
+        "android.hardware.automotive.audiocontrol-V3-cpp",
         "libgmock",
     ],
     test_suites: [
diff --git a/automotive/audiocontrol/aidl/vts/VtsHalAudioControlTargetTest.cpp b/automotive/audiocontrol/aidl/vts/VtsHalAudioControlTargetTest.cpp
index f4f5eef..f4e3b5a 100644
--- a/automotive/audiocontrol/aidl/vts/VtsHalAudioControlTargetTest.cpp
+++ b/automotive/audiocontrol/aidl/vts/VtsHalAudioControlTargetTest.cpp
@@ -21,6 +21,7 @@
 
 #include <android/hardware/automotive/audiocontrol/BnAudioGainCallback.h>
 #include <android/hardware/automotive/audiocontrol/BnFocusListener.h>
+#include <android/hardware/automotive/audiocontrol/BnModuleChangeCallback.h>
 #include <android/hardware/automotive/audiocontrol/IAudioControl.h>
 #include <android/log.h>
 #include <binder/IServiceManager.h>
@@ -34,10 +35,14 @@
 using android::hardware::automotive::audiocontrol::AudioGainConfigInfo;
 using android::hardware::automotive::audiocontrol::BnAudioGainCallback;
 using android::hardware::automotive::audiocontrol::BnFocusListener;
+using android::hardware::automotive::audiocontrol::BnModuleChangeCallback;
 using android::hardware::automotive::audiocontrol::DuckingInfo;
 using android::hardware::automotive::audiocontrol::IAudioControl;
+using android::hardware::automotive::audiocontrol::IModuleChangeCallback;
 using android::hardware::automotive::audiocontrol::MutingInfo;
 using android::hardware::automotive::audiocontrol::Reasons;
+using ::testing::AnyOf;
+using ::testing::Eq;
 
 #include "android_audio_policy_configuration_V7_0.h"
 
@@ -55,6 +60,8 @@
         ASSERT_NE(audioControl, nullptr);
     }
 
+    void TearDown() override { audioControl = nullptr; }
+
     sp<IAudioControl> audioControl;
     int32_t capabilities;
 };
@@ -226,6 +233,47 @@
     ASSERT_TRUE(audioControl->registerGainCallback(gainCallback2).isOk());
 }
 
+/*
+ * Test Module change Callback registration.
+ *
+ * Verifies that:
+ * - setModuleChangeCallback succeeds
+ * - setting a double callback fails with exception
+ * - clearModuleChangeCallback succeeds
+ * - setting with nullptr callback fails with exception
+ * - closing handle does not crash
+ */
+struct ModuleChangeCallbackMock : BnModuleChangeCallback {
+    MOCK_METHOD(Status, onAudioPortsChanged,
+                (const std::vector<android::media::audio::common::AudioPort>& audioPorts));
+};
+
+TEST_P(AudioControlAidl, RegisterModuleChangeCallbackTwiceThrowsException) {
+    ALOGI("Register Module change callback test");
+    // make sure no stale callbacks.
+    audioControl->clearModuleChangeCallback();
+
+    sp<ModuleChangeCallbackMock> moduleChangeCallback = new ModuleChangeCallbackMock();
+    auto status = audioControl->setModuleChangeCallback(moduleChangeCallback);
+    EXPECT_THAT(status.exceptionCode(),
+                AnyOf(Eq(Status::EX_NONE), Eq(Status::EX_UNSUPPORTED_OPERATION)));
+    if (!status.isOk()) return;
+
+    sp<ModuleChangeCallbackMock> moduleChangeCallback2 = new ModuleChangeCallbackMock();
+    // no need to check for unsupported feature
+    EXPECT_EQ(Status::EX_ILLEGAL_STATE,
+              audioControl->setModuleChangeCallback(moduleChangeCallback2).exceptionCode());
+    ASSERT_TRUE(audioControl->clearModuleChangeCallback().isOk());
+    ASSERT_TRUE(audioControl->setModuleChangeCallback(moduleChangeCallback2).isOk());
+}
+
+TEST_P(AudioControlAidl, RegisterModuleChangeNullCallbackThrowsException) {
+    ALOGI("Register Module change callback with nullptr test");
+    auto status = audioControl->setModuleChangeCallback(nullptr);
+    EXPECT_THAT(status.exceptionCode(),
+                AnyOf(Eq(Status::EX_ILLEGAL_ARGUMENT), Eq(Status::EX_UNSUPPORTED_OPERATION)));
+}
+
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioControlAidl);
 INSTANTIATE_TEST_SUITE_P(
         Audiocontrol, AudioControlAidl,